2015-06-01 03:56:28 +08:00
|
|
|
/**
|
|
|
|
* Created by Estevao on 31-05-2015.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Showdown Converter class
|
|
|
|
* @class
|
|
|
|
* @param {object} [converterOptions]
|
2015-08-23 09:21:00 +08:00
|
|
|
* @returns {Converter}
|
2015-06-01 03:56:28 +08:00
|
|
|
*/
|
|
|
|
showdown.Converter = function (converterOptions) {
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
var
|
|
|
|
/**
|
|
|
|
* Options used by this converter
|
|
|
|
* @private
|
|
|
|
* @type {{}}
|
|
|
|
*/
|
2015-07-11 22:45:58 +08:00
|
|
|
options = {},
|
2015-06-01 03:56:28 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Language extensions used by this converter
|
|
|
|
* @private
|
|
|
|
* @type {Array}
|
|
|
|
*/
|
|
|
|
langExtensions = [],
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Output modifiers extensions used by this converter
|
|
|
|
* @private
|
|
|
|
* @type {Array}
|
|
|
|
*/
|
|
|
|
outputModifiers = [],
|
|
|
|
|
|
|
|
/**
|
2015-10-19 10:20:20 +08:00
|
|
|
* Event listeners
|
2015-06-01 03:56:28 +08:00
|
|
|
* @private
|
2015-10-19 10:20:20 +08:00
|
|
|
* @type {{}}
|
2015-06-01 03:56:28 +08:00
|
|
|
*/
|
2017-01-09 03:09:12 +08:00
|
|
|
listeners = {},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The flavor set in this converter
|
|
|
|
*/
|
2017-12-10 15:15:09 +08:00
|
|
|
setConvFlavor = setFlavor,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Metadata of the document
|
|
|
|
* @type {{parsed: {}, raw: string, format: string}}
|
|
|
|
*/
|
|
|
|
metadata = {
|
|
|
|
parsed: {},
|
|
|
|
raw: '',
|
|
|
|
format: ''
|
|
|
|
};
|
2015-06-01 03:56:28 +08:00
|
|
|
|
|
|
|
_constructor();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converter constructor
|
|
|
|
* @private
|
|
|
|
*/
|
2017-01-31 13:46:25 +08:00
|
|
|
function _constructor () {
|
2015-06-01 03:56:28 +08:00
|
|
|
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];
|
|
|
|
}
|
|
|
|
}
|
2015-06-03 08:29:44 +08:00
|
|
|
} else {
|
|
|
|
throw Error('Converter expects the passed parameter to be an object, but ' + typeof converterOptions +
|
2015-06-03 09:50:57 +08:00
|
|
|
' was passed instead.');
|
2015-06-01 03:56:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (options.extensions) {
|
|
|
|
showdown.helper.forEach(options.extensions, _parseExtension);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse extension
|
|
|
|
* @param {*} ext
|
2015-07-13 12:09:03 +08:00
|
|
|
* @param {string} [name='']
|
2015-06-01 03:56:28 +08:00
|
|
|
* @private
|
|
|
|
*/
|
2017-01-31 13:46:25 +08:00
|
|
|
function _parseExtension (ext, name) {
|
2015-06-01 03:56:28 +08:00
|
|
|
|
2015-07-13 12:09:03 +08:00
|
|
|
name = name || null;
|
2015-06-01 03:56:28 +08:00
|
|
|
// 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;
|
2015-06-01 03:56:28 +08:00
|
|
|
|
2015-06-08 02:02:45 +08:00
|
|
|
// LEGACY_SUPPORT CODE
|
|
|
|
if (showdown.extensions[ext]) {
|
|
|
|
console.warn('DEPRECATION WARNING: ' + ext + ' is an old extension that uses a deprecated loading method.' +
|
2015-06-01 03:56:28 +08:00
|
|
|
'Please inform the developer that the extension should be updated!');
|
2015-06-08 02:02:45 +08:00
|
|
|
legacyExtensionLoading(showdown.extensions[ext], ext);
|
|
|
|
return;
|
2015-06-01 03:56:28 +08:00
|
|
|
// 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.');
|
|
|
|
}
|
2015-06-08 02:02:45 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof ext === 'function') {
|
2015-06-01 03:56:28 +08:00
|
|
|
ext = ext();
|
|
|
|
}
|
|
|
|
|
2015-06-08 02:02:45 +08:00
|
|
|
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);
|
2015-06-01 03:56:28 +08:00
|
|
|
}
|
|
|
|
|
2015-06-08 02:02:45 +08:00
|
|
|
for (var i = 0; i < ext.length; ++i) {
|
|
|
|
switch (ext[i].type) {
|
2015-08-03 10:47:49 +08:00
|
|
|
|
2015-06-08 02:02:45 +08:00
|
|
|
case 'lang':
|
|
|
|
langExtensions.push(ext[i]);
|
|
|
|
break;
|
2015-06-01 03:56:28 +08:00
|
|
|
|
2015-06-08 02:02:45 +08:00
|
|
|
case 'output':
|
|
|
|
outputModifiers.push(ext[i]);
|
|
|
|
break;
|
2015-08-03 10:47:49 +08:00
|
|
|
}
|
2016-12-01 23:48:30 +08:00
|
|
|
if (ext[i].hasOwnProperty('listeners')) {
|
2015-08-03 10:47:49 +08:00
|
|
|
for (var ln in ext[i].listeners) {
|
|
|
|
if (ext[i].listeners.hasOwnProperty(ln)) {
|
|
|
|
listen(ln, ext[i].listeners[ln]);
|
|
|
|
}
|
|
|
|
}
|
2015-06-08 02:02:45 +08:00
|
|
|
}
|
|
|
|
}
|
2015-08-03 10:47:49 +08:00
|
|
|
|
2015-06-08 02:02:45 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* LEGACY_SUPPORT
|
|
|
|
* @param {*} ext
|
|
|
|
* @param {string} name
|
|
|
|
*/
|
2017-01-31 13:46:25 +08:00
|
|
|
function legacyExtensionLoading (ext, name) {
|
2015-06-08 02:02:45 +08:00
|
|
|
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!!!');
|
|
|
|
}
|
2015-06-01 03:56:28 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-08-03 10:47:49 +08:00
|
|
|
/**
|
|
|
|
* Listen to an event
|
|
|
|
* @param {string} name
|
|
|
|
* @param {function} callback
|
|
|
|
*/
|
2017-01-31 13:46:25 +08:00
|
|
|
function listen (name, callback) {
|
2015-08-03 10:47:49 +08:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-01-31 13:46:25 +08:00
|
|
|
function rTrimInputText (text) {
|
2016-06-07 08:23:52 +08:00
|
|
|
var rsp = text.match(/^\s*/)[0].length,
|
|
|
|
rgx = new RegExp('^\\s{0,' + rsp + '}', 'gm');
|
|
|
|
return text.replace(rgx, '');
|
|
|
|
}
|
|
|
|
|
2015-08-03 10:47:49 +08:00
|
|
|
/**
|
|
|
|
* Dispatch an event
|
|
|
|
* @private
|
|
|
|
* @param {string} evtName Event name
|
|
|
|
* @param {string} text Text
|
|
|
|
* @param {{}} options Converter Options
|
2016-03-21 01:08:44 +08:00
|
|
|
* @param {{}} globals
|
2015-08-03 10:47:49 +08:00
|
|
|
* @returns {string}
|
|
|
|
*/
|
2016-03-21 01:08:44 +08:00
|
|
|
this._dispatch = function dispatch (evtName, text, options, globals) {
|
2015-08-03 10:47:49 +08:00
|
|
|
if (listeners.hasOwnProperty(evtName)) {
|
|
|
|
for (var ei = 0; ei < listeners[evtName].length; ++ei) {
|
2016-03-21 01:08:44 +08:00
|
|
|
var nText = listeners[evtName][ei](evtName, text, this, options, globals);
|
2015-08-03 10:47:49 +08:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2015-06-01 03:56:28 +08:00
|
|
|
/**
|
|
|
|
* 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: [],
|
2016-01-02 09:08:17 +08:00
|
|
|
gHtmlMdBlocks: [],
|
2015-10-19 08:55:35 +08:00
|
|
|
gHtmlSpans: [],
|
2015-06-01 03:56:28 +08:00
|
|
|
gUrls: {},
|
|
|
|
gTitles: {},
|
2015-06-17 08:22:05 +08:00
|
|
|
gDimensions: {},
|
2015-06-01 03:56:28 +08:00
|
|
|
gListLevel: 0,
|
|
|
|
hashLinkCounts: {},
|
|
|
|
langExtensions: langExtensions,
|
|
|
|
outputModifiers: outputModifiers,
|
2016-01-25 09:04:06 +08:00
|
|
|
converter: this,
|
2017-12-10 15:15:09 +08:00
|
|
|
ghCodeBlocks: [],
|
|
|
|
metadata: {
|
|
|
|
parsed: {},
|
|
|
|
raw: '',
|
|
|
|
format: ''
|
|
|
|
}
|
2015-06-01 03:56:28 +08:00
|
|
|
};
|
|
|
|
|
2017-01-29 08:07:19 +08:00
|
|
|
// This lets us use ¨ trema as an escape char to avoid md5 hashes
|
2015-06-01 03:56:28 +08:00
|
|
|
// The choice of character is arbitrary; anything that isn't
|
|
|
|
// magic in Markdown will work.
|
2017-01-29 08:07:19 +08:00
|
|
|
text = text.replace(/¨/g, '¨T');
|
2015-06-01 03:56:28 +08:00
|
|
|
|
2017-01-29 08:07:19 +08:00
|
|
|
// Replace $ with ¨D
|
2015-06-01 03:56:28 +08:00
|
|
|
// RegExp interprets $ as a special character
|
|
|
|
// when it's in a replacement string
|
2017-01-29 08:07:19 +08:00
|
|
|
text = text.replace(/\$/g, '¨D');
|
2015-06-01 03:56:28 +08:00
|
|
|
|
|
|
|
// Standardize line endings
|
|
|
|
text = text.replace(/\r\n/g, '\n'); // DOS to Unix
|
|
|
|
text = text.replace(/\r/g, '\n'); // Mac to Unix
|
|
|
|
|
2016-12-17 13:26:15 +08:00
|
|
|
// Stardardize line spaces (nbsp causes trouble in older browsers and some regex flavors)
|
|
|
|
text = text.replace(/\u00A0/g, ' ');
|
|
|
|
|
2016-06-07 08:23:52 +08:00
|
|
|
if (options.smartIndentationFix) {
|
|
|
|
text = rTrimInputText(text);
|
|
|
|
}
|
|
|
|
|
2015-06-01 03:56:28 +08:00
|
|
|
// 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);
|
|
|
|
|
2017-01-30 03:28:30 +08:00
|
|
|
/**
|
|
|
|
* 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, '');
|
2015-06-01 03:56:28 +08:00
|
|
|
|
|
|
|
//run languageExtensions
|
|
|
|
showdown.helper.forEach(langExtensions, function (ext) {
|
|
|
|
text = showdown.subParser('runExtension')(ext, text, options, globals);
|
|
|
|
});
|
|
|
|
|
2015-08-03 10:47:49 +08:00
|
|
|
// run the sub parsers
|
2017-12-10 15:15:09 +08:00
|
|
|
text = showdown.subParser('metadata')(text, options, globals);
|
2016-01-25 11:01:54 +08:00
|
|
|
text = showdown.subParser('hashPreCodeTags')(text, options, globals);
|
2015-08-03 10:47:49 +08:00
|
|
|
text = showdown.subParser('githubCodeBlocks')(text, options, globals);
|
|
|
|
text = showdown.subParser('hashHTMLBlocks')(text, options, globals);
|
2017-02-06 11:28:49 +08:00
|
|
|
text = showdown.subParser('hashCodeTags')(text, options, globals);
|
2015-08-03 10:47:49 +08:00
|
|
|
text = showdown.subParser('stripLinkDefinitions')(text, options, globals);
|
|
|
|
text = showdown.subParser('blockGamut')(text, options, globals);
|
2015-10-19 10:20:20 +08:00
|
|
|
text = showdown.subParser('unhashHTMLSpans')(text, options, globals);
|
2015-08-03 10:47:49 +08:00
|
|
|
text = showdown.subParser('unescapeSpecialChars')(text, options, globals);
|
2015-06-01 03:56:28 +08:00
|
|
|
|
|
|
|
// attacklab: Restore dollar signs
|
2017-01-29 08:07:19 +08:00
|
|
|
text = text.replace(/¨D/g, '$$');
|
2015-06-01 03:56:28 +08:00
|
|
|
|
2017-01-29 08:07:19 +08:00
|
|
|
// attacklab: Restore tremas
|
|
|
|
text = text.replace(/¨T/g, '¨');
|
2015-06-01 03:56:28 +08:00
|
|
|
|
2017-12-10 12:49:24 +08:00
|
|
|
// render a complete html document instead of a partial if the option is enabled
|
2017-12-10 15:15:09 +08:00
|
|
|
text = showdown.subParser('completeHTMLDocument')(text, options, globals);
|
2017-12-10 12:49:24 +08:00
|
|
|
|
2015-06-01 03:56:28 +08:00
|
|
|
// Run output modifiers
|
|
|
|
showdown.helper.forEach(outputModifiers, function (ext) {
|
|
|
|
text = showdown.subParser('runExtension')(ext, text, options, globals);
|
|
|
|
});
|
|
|
|
|
2017-12-10 15:15:09 +08:00
|
|
|
// update metadata
|
|
|
|
metadata = globals.metadata;
|
2015-06-01 03:56:28 +08:00
|
|
|
return text;
|
|
|
|
};
|
|
|
|
|
2017-11-27 14:44:25 +08:00
|
|
|
this.makeMarkdown = function (src) {
|
|
|
|
|
|
|
|
// replace \r\n with \n
|
|
|
|
src = src.replace(/\r\n/g, '\n');
|
|
|
|
src = src.replace(/\r/g, '\n'); // old macs
|
|
|
|
|
|
|
|
// due to an edge case, we need to find this: > <
|
|
|
|
// to prevent removing of non silent white spaces
|
|
|
|
// ex: <em>this is</em> <strong>sparta</strong>
|
|
|
|
src = src.replace(/>[ \t]+</, '>¨NBSP;<');
|
|
|
|
|
|
|
|
var doc = document.createElement('div');
|
|
|
|
doc.innerHTML = src;
|
|
|
|
|
|
|
|
var preList = substitutePreCodeTags(doc);
|
|
|
|
|
|
|
|
// remove all newlines and collapse spaces
|
|
|
|
clean(doc);
|
|
|
|
|
|
|
|
function parseNode (node, spansOnly) {
|
|
|
|
|
|
|
|
spansOnly = spansOnly || false;
|
|
|
|
|
|
|
|
var txt = '';
|
|
|
|
//indent = new Array((indentationLevel * 4) + 1).join(' ');
|
|
|
|
|
|
|
|
// edge case of text without wrapper paragraph
|
|
|
|
if (node.nodeType === 3) {
|
|
|
|
return parseTxt(node);
|
|
|
|
}
|
|
|
|
|
|
|
|
// HTML comment
|
|
|
|
if (node.nodeType === 8) {
|
|
|
|
// TODO parse comments
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
// process only node elements
|
|
|
|
if (node.nodeType !== 1) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
var tagName = node.tagName.toLowerCase();
|
|
|
|
|
|
|
|
switch (tagName) {
|
|
|
|
|
|
|
|
//
|
|
|
|
// BLOCKS
|
|
|
|
//
|
|
|
|
case 'h1':
|
|
|
|
if (!spansOnly) { txt = parseHeader(node, 1) + '\n\n'; }
|
|
|
|
break;
|
|
|
|
case 'h2':
|
|
|
|
if (!spansOnly) { txt = parseHeader(node, 2) + '\n\n'; }
|
|
|
|
break;
|
|
|
|
case 'h3':
|
|
|
|
if (!spansOnly) { txt = parseHeader(node, 3) + '\n\n'; }
|
|
|
|
break;
|
|
|
|
case 'h4':
|
|
|
|
if (!spansOnly) { txt = parseHeader(node, 4) + '\n\n'; }
|
|
|
|
break;
|
|
|
|
case 'h5':
|
|
|
|
if (!spansOnly) { txt = parseHeader(node, 5) + '\n\n'; }
|
|
|
|
break;
|
|
|
|
case 'h6':
|
|
|
|
if (!spansOnly) { txt = parseHeader(node, 6) + '\n\n'; }
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'p':
|
|
|
|
if (!spansOnly) { txt = parseParagraph(node) + '\n\n'; }
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'blockquote':
|
|
|
|
if (!spansOnly) { txt = parseBlockquote(node) + '\n\n'; }
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'hr':
|
|
|
|
if (!spansOnly) { txt = parseHr(node) + '\n\n'; }
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'ol':
|
|
|
|
if (!spansOnly) { txt = parseList(node, 'ol') + '\n\n'; }
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'ul':
|
|
|
|
if (!spansOnly) { txt = parseList(node, 'ul') + '\n\n'; }
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'precode':
|
|
|
|
if (!spansOnly) { txt = parsePreCode(node) + '\n\n'; }
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'pre':
|
|
|
|
if (!spansOnly) { txt = parsePre(node) + '\n\n'; }
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'table':
|
|
|
|
if (!spansOnly) { txt = parseTable(node) + '\n\n'; }
|
|
|
|
break;
|
|
|
|
|
|
|
|
//
|
|
|
|
// SPANS
|
|
|
|
//
|
|
|
|
case 'code':
|
|
|
|
txt = parseCodeSpan(node);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'em':
|
|
|
|
case 'i':
|
|
|
|
txt = parseEmphasis(node);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'strong':
|
|
|
|
case 'b':
|
|
|
|
txt = parseStrong(node);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'del':
|
|
|
|
txt = parseDel(node);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'a':
|
|
|
|
txt = parseLinks(node);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'img':
|
|
|
|
txt = parseImage(node);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
txt = node.innerHTML;
|
|
|
|
}
|
|
|
|
|
|
|
|
return txt;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseTxt (node) {
|
|
|
|
var txt = node.nodeValue;
|
|
|
|
|
|
|
|
txt = txt.replace(/¨NBSP;/g, ' ');
|
|
|
|
|
|
|
|
// escape markdown magic characters
|
|
|
|
// emphasis, strong and strikethrough - can appear everywhere
|
|
|
|
// we also escape pipe (\) because of tables
|
|
|
|
// and escape ` because of code blocks and spans
|
|
|
|
txt = txt.replace(/([*_~|`])/g, '\\$1');
|
|
|
|
|
|
|
|
// escape > because of blockquotes
|
|
|
|
txt = txt.replace(/^(\s*)>/g, '\\$1>');
|
|
|
|
|
|
|
|
// hash character, only troublesome at the beginning of a line because of headers
|
|
|
|
txt = txt.replace(/^#/gm, '\\#');
|
|
|
|
|
|
|
|
// horizontal rules
|
|
|
|
txt = txt.replace(/^(\s*)([-=]{3,})(\s*)$/, '$1\\$2$3');
|
|
|
|
|
|
|
|
// dot, because of ordered lists, only troublesome at the beginning of a line when preceded by an integer
|
|
|
|
txt = txt.replace(/^( {0,3}\d+)\./gm, '$1\\.');
|
|
|
|
|
|
|
|
// + and -, at the beginning of a line becomes a list, so we need to escape them also
|
|
|
|
txt = txt.replace(/^( {0,3})([+-])/gm, '$1\\$2');
|
|
|
|
|
|
|
|
// images and links, ] followed by ( is problematic, so we escape it
|
|
|
|
// same for reference style uris
|
|
|
|
// might be a bit overzealous, but we prefer to be safe
|
|
|
|
txt = txt.replace(/]([\s]*)\(/g, '\\]$1\\(');
|
|
|
|
txt = txt.replace(/\[([\s\S]*)]:/g, '\\[$1\\]:');
|
|
|
|
|
|
|
|
return txt;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseList (node, type) {
|
|
|
|
var txt = '';
|
|
|
|
if (!node.hasChildNodes()) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
var listItems = node.childNodes,
|
|
|
|
listItemsLenght = listItems.length,
|
|
|
|
listNum = 1;
|
|
|
|
|
|
|
|
for (var i = 0; i < listItemsLenght; ++i) {
|
|
|
|
if (typeof listItems[i].tagName === 'undefined' || listItems[i].tagName.toLowerCase() !== 'li') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// define the bullet to use in list
|
|
|
|
var bullet = '';
|
|
|
|
if (type === 'ol') {
|
|
|
|
bullet = listNum.toString() + '. ';
|
|
|
|
} else {
|
|
|
|
bullet = '- ';
|
|
|
|
}
|
|
|
|
|
|
|
|
// parse list item
|
|
|
|
txt += bullet + parseListItem(listItems[i]);
|
|
|
|
++listNum;
|
|
|
|
}
|
|
|
|
|
|
|
|
return txt.trim();
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseListItem (node) {
|
|
|
|
var listItemTxt = '';
|
|
|
|
|
|
|
|
var children = node.childNodes,
|
|
|
|
childrenLenght = children.length;
|
|
|
|
|
|
|
|
for (var i = 0; i < childrenLenght; ++i) {
|
|
|
|
listItemTxt += parseNode(children[i]);
|
|
|
|
}
|
|
|
|
// if it's only one liner, we need to add a newline at the end
|
|
|
|
if (!/\n$/.test(listItemTxt)) {
|
|
|
|
listItemTxt += '\n';
|
|
|
|
} else {
|
|
|
|
// it's multiparagraph, so we need to indent
|
|
|
|
listItemTxt = listItemTxt
|
|
|
|
.split('\n')
|
|
|
|
.join('\n ')
|
|
|
|
.replace(/^ {4}$/gm, '')
|
|
|
|
.replace(/\n\n+/g, '\n\n');
|
|
|
|
}
|
|
|
|
|
|
|
|
return listItemTxt;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseHr () {
|
|
|
|
return '---';
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseBlockquote (node) {
|
|
|
|
var txt = '';
|
|
|
|
if (node.hasChildNodes()) {
|
|
|
|
var children = node.childNodes,
|
|
|
|
childrenLength = children.length;
|
|
|
|
|
|
|
|
for (var i = 0; i < childrenLength; ++i) {
|
|
|
|
var innerTxt = parseNode(children[i]);
|
|
|
|
|
|
|
|
if (innerTxt === '') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
txt += innerTxt;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// cleanup
|
|
|
|
txt = txt.trim();
|
|
|
|
txt = '> ' + txt.split('\n').join('\n> ');
|
|
|
|
return txt;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseCodeSpan (node) {
|
|
|
|
return '`' + node.innerHTML + '`';
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseStrong (node) {
|
|
|
|
var txt = '';
|
|
|
|
if (node.hasChildNodes()) {
|
|
|
|
txt += '**';
|
|
|
|
var children = node.childNodes,
|
|
|
|
childrenLength = children.length;
|
|
|
|
for (var i = 0; i < childrenLength; ++i) {
|
|
|
|
txt += parseNode(children[i]);
|
|
|
|
}
|
|
|
|
txt += '**';
|
|
|
|
}
|
|
|
|
return txt;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseEmphasis (node) {
|
|
|
|
var txt = '';
|
|
|
|
if (node.hasChildNodes()) {
|
|
|
|
txt += '*';
|
|
|
|
var children = node.childNodes,
|
|
|
|
childrenLength = children.length;
|
|
|
|
for (var i = 0; i < childrenLength; ++i) {
|
|
|
|
txt += parseNode(children[i]);
|
|
|
|
}
|
|
|
|
txt += '*';
|
|
|
|
}
|
|
|
|
return txt;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseDel (node) {
|
|
|
|
var txt = '';
|
|
|
|
if (node.hasChildNodes()) {
|
|
|
|
txt += '~~';
|
|
|
|
var children = node.childNodes,
|
|
|
|
childrenLength = children.length;
|
|
|
|
for (var i = 0; i < childrenLength; ++i) {
|
|
|
|
txt += parseNode(children[i]);
|
|
|
|
}
|
|
|
|
txt += '~~';
|
|
|
|
}
|
|
|
|
return txt;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseLinks (node) {
|
|
|
|
var txt = '';
|
|
|
|
if (node.hasChildNodes() && node.hasAttribute('href')) {
|
|
|
|
var children = node.childNodes,
|
|
|
|
childrenLength = children.length;
|
|
|
|
txt = '[';
|
|
|
|
for (var i = 0; i < childrenLength; ++i) {
|
|
|
|
txt += parseNode(children[i]);
|
|
|
|
}
|
|
|
|
txt += ']';
|
|
|
|
txt += '(' + node.getAttribute('href') + ')';
|
|
|
|
}
|
|
|
|
return txt;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseImage (node) {
|
|
|
|
var txt = '';
|
|
|
|
if (node.hasAttribute('src')) {
|
|
|
|
txt += '![' + node.getAttribute('alt') + ']';
|
|
|
|
txt += '(' + node.getAttribute('src');
|
|
|
|
if (node.hasAttribute('width') && node.hasAttribute('height')) {
|
|
|
|
txt += ' =' + node.getAttribute('width') + 'x' + node.getAttribute('height');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node.hasAttribute('title')) {
|
|
|
|
txt += ' "' + node.getAttribute('title') + '"';
|
|
|
|
}
|
|
|
|
txt += ')';
|
|
|
|
}
|
|
|
|
return txt;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseHeader (node, headerLevel) {
|
|
|
|
var headerMark = new Array(headerLevel + 1).join('#'),
|
|
|
|
txt = '';
|
|
|
|
|
|
|
|
if (node.hasChildNodes()) {
|
|
|
|
txt = headerMark + ' ';
|
|
|
|
var children = node.childNodes,
|
|
|
|
childrenLength = children.length;
|
|
|
|
|
|
|
|
for (var i = 0; i < childrenLength; ++i) {
|
|
|
|
txt += parseNode(children[i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return txt;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseParagraph (node) {
|
|
|
|
var txt = '';
|
|
|
|
if (node.hasChildNodes()) {
|
|
|
|
var children = node.childNodes,
|
|
|
|
childrenLength = children.length;
|
|
|
|
for (var i = 0; i < childrenLength; ++i) {
|
|
|
|
txt += parseNode(children[i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return txt;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parsePreCode (node) {
|
|
|
|
var lang = node.getAttribute('language'),
|
|
|
|
num = node.getAttribute('precodenum');
|
|
|
|
return '```' + lang + '\n' + preList[num] + '\n```';
|
|
|
|
}
|
|
|
|
|
|
|
|
function parsePre (node) {
|
|
|
|
var num = node.getAttribute('prenum');
|
|
|
|
return '<pre>' + preList[num] + '</pre>';
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseTable (node) {
|
|
|
|
|
|
|
|
var txt = '',
|
|
|
|
tableArray = [[], []],
|
|
|
|
headings = node.querySelectorAll('thead>tr>th'),
|
|
|
|
rows = node.querySelectorAll('tbody>tr'),
|
|
|
|
i, ii;
|
|
|
|
for (i = 0; i < headings.length; ++i) {
|
|
|
|
var headContent = parseTableCell(headings[i]),
|
|
|
|
allign = '---';
|
|
|
|
|
|
|
|
if (headings[i].hasAttribute('style')) {
|
|
|
|
var style = headings[i].getAttribute('style').toLowerCase().replace(/\s/g, '');
|
|
|
|
switch (style) {
|
|
|
|
case 'text-align:left;':
|
|
|
|
allign = ':---';
|
|
|
|
break;
|
|
|
|
case 'text-align:right;':
|
|
|
|
allign = '---:';
|
|
|
|
break;
|
|
|
|
case 'text-align:center;':
|
|
|
|
allign = ':---:';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
tableArray[0][i] = headContent.trim();
|
|
|
|
tableArray[1][i] = allign;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (i = 0; i < rows.length; ++i) {
|
|
|
|
var r = tableArray.push([]) - 1,
|
|
|
|
cols = rows[i].getElementsByTagName('td');
|
|
|
|
|
|
|
|
for (ii = 0; ii < headings.length; ++ii) {
|
|
|
|
var cellContent = ' ';
|
|
|
|
if (typeof cols[ii] !== 'undefined') {
|
|
|
|
cellContent = parseTableCell(cols[ii]);
|
|
|
|
}
|
|
|
|
tableArray[r].push(cellContent);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var cellSpacesCount = 3;
|
|
|
|
for (i = 0; i < tableArray.length; ++i) {
|
|
|
|
for (ii = 0; ii < tableArray[i].length; ++ii) {
|
|
|
|
var strLen = tableArray[i][ii].length;
|
|
|
|
if (strLen > cellSpacesCount) {
|
|
|
|
cellSpacesCount = strLen;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (i = 0; i < tableArray.length; ++i) {
|
|
|
|
for (ii = 0; ii < tableArray[i].length; ++ii) {
|
|
|
|
if (i === 1) {
|
|
|
|
if (tableArray[i][ii].slice(-1) === ':') {
|
|
|
|
tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii].slice(-1), cellSpacesCount - 1, '-') + ':';
|
|
|
|
} else {
|
|
|
|
tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount, '-');
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
txt += '| ' + tableArray[i].join(' | ') + ' |\n';
|
|
|
|
}
|
|
|
|
|
|
|
|
return txt.trim();
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseTableCell (node) {
|
|
|
|
var txt = '';
|
|
|
|
if (!node.hasChildNodes()) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
var children = node.childNodes,
|
|
|
|
childrenLength = children.length;
|
|
|
|
|
|
|
|
for (var i = 0; i < childrenLength; ++i) {
|
|
|
|
txt += parseNode(children[i], true);
|
|
|
|
}
|
|
|
|
return txt.trim();
|
|
|
|
}
|
|
|
|
|
|
|
|
function clean (node) {
|
|
|
|
for (var n = 0; n < node.childNodes.length; ++n) {
|
|
|
|
var child = node.childNodes[n];
|
|
|
|
if (child.nodeType === 3) {
|
|
|
|
if (!/\S/.test(child.nodeValue)) {
|
|
|
|
node.removeChild(child);
|
|
|
|
--n;
|
|
|
|
} else {
|
|
|
|
child.nodeValue = child.nodeValue.split('\n').join(' ');
|
|
|
|
child.nodeValue = child.nodeValue.replace(/(\s)+/g, '$1');
|
|
|
|
}
|
|
|
|
} else if (child.nodeType === 1) {
|
|
|
|
clean(child);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// find all pre tags and replace contents with placeholder
|
|
|
|
// we need this so that we can remove all indentation from html
|
|
|
|
// to ease up parsing
|
|
|
|
function substitutePreCodeTags (doc) {
|
|
|
|
|
|
|
|
var pres = doc.querySelectorAll('pre'),
|
|
|
|
presPH = [];
|
|
|
|
|
|
|
|
for (var i = 0; i < pres.length; ++i) {
|
|
|
|
|
|
|
|
if (pres[i].childElementCount === 1 && pres[i].firstChild.tagName.toLowerCase() === 'code') {
|
|
|
|
var content = pres[i].firstChild.innerHTML,
|
|
|
|
language = pres[i].firstChild.getAttribute('data-language') || '';
|
|
|
|
presPH.push(content);
|
|
|
|
pres[i].outerHTML = '<precode language="' + language + '" precodenum="' + i.toString() + '"></precode>';
|
|
|
|
} else {
|
|
|
|
presPH.push(pres[i].innerHTML);
|
|
|
|
pres[i].innerHTML = '';
|
|
|
|
pres[i].setAttribute('prenum', i.toString());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return presPH;
|
|
|
|
}
|
|
|
|
|
|
|
|
var nodes = doc.childNodes,
|
|
|
|
mdDoc = '';
|
|
|
|
|
|
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
|
|
mdDoc += parseNode(nodes[i]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return mdDoc;
|
|
|
|
};
|
|
|
|
|
2015-06-01 03:56:28 +08:00
|
|
|
/**
|
|
|
|
* 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-06-01 03:56:28 +08:00
|
|
|
*/
|
2015-07-13 12:09:03 +08:00
|
|
|
this.addExtension = function (extension, name) {
|
|
|
|
name = name || null;
|
|
|
|
_parseExtension(extension, name);
|
2015-06-01 03:56:28 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2015-06-08 02:02:45 +08:00
|
|
|
* Use a global registered extension with THIS converter
|
|
|
|
* @param {string} extensionName Name of the previously registered extension
|
|
|
|
*/
|
|
|
|
this.useExtension = function (extensionName) {
|
|
|
|
_parseExtension(extensionName);
|
|
|
|
};
|
|
|
|
|
2015-07-12 09:15:35 +08:00
|
|
|
/**
|
|
|
|
* Set the flavor THIS converter should use
|
|
|
|
* @param {string} name
|
|
|
|
*/
|
|
|
|
this.setFlavor = function (name) {
|
2017-01-09 03:09:12 +08:00
|
|
|
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];
|
2015-07-12 09:15:35 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-01-09 03:09:12 +08:00
|
|
|
/**
|
|
|
|
* Get the currently set flavor of this converter
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
this.getFlavor = function () {
|
|
|
|
return setConvFlavor;
|
|
|
|
};
|
|
|
|
|
2015-06-08 02:02:45 +08:00
|
|
|
/**
|
|
|
|
* 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
|
2015-06-01 03:56:28 +08:00
|
|
|
*/
|
|
|
|
this.removeExtension = function (extension) {
|
2015-06-08 02:02:45 +08:00
|
|
|
if (!showdown.helper.isArray(extension)) {
|
|
|
|
extension = [extension];
|
2015-06-01 03:56:28 +08:00
|
|
|
}
|
2015-06-08 02:02:45 +08:00
|
|
|
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);
|
|
|
|
}
|
2015-06-01 03:56:28 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all extension of THIS converter
|
|
|
|
* @returns {{language: Array, output: Array}}
|
|
|
|
*/
|
|
|
|
this.getAllExtensions = function () {
|
|
|
|
return {
|
|
|
|
language: langExtensions,
|
|
|
|
output: outputModifiers
|
|
|
|
};
|
|
|
|
};
|
2017-12-10 15:15:09 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the metadata of the previously parsed document
|
|
|
|
* @param raw
|
|
|
|
* @returns {string|{}}
|
|
|
|
*/
|
|
|
|
this.getMetadata = function (raw) {
|
|
|
|
if (raw) {
|
|
|
|
return metadata.raw;
|
|
|
|
} else {
|
|
|
|
return metadata.parsed;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the metadata format of the previously parsed document
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
this.getMetadataFormat = function () {
|
|
|
|
return metadata.format;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Private: set a single key, value metadata pair
|
|
|
|
* @param {string} key
|
|
|
|
* @param {string} value
|
|
|
|
*/
|
|
|
|
this._setMetadataPair = function (key, value) {
|
|
|
|
metadata.parsed[key] = value;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Private: set metadata format
|
|
|
|
* @param {string} format
|
|
|
|
*/
|
|
|
|
this._setMetadataFormat = function (format) {
|
|
|
|
metadata.format = format;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Private: set metadata raw text
|
|
|
|
* @param {string} raw
|
|
|
|
*/
|
|
|
|
this._setMetadataRaw = function (raw) {
|
|
|
|
metadata.raw = raw;
|
|
|
|
};
|
2015-06-01 03:56:28 +08:00
|
|
|
};
|