(function () { // A quick way to make sure we're only keeping span-level tags when we need to. // This isn't supposed to be foolproof. It's just a quick way to make sure we // keep all span-level tags returned by a pagedown converter. It should allow // all span-level tags through, with or without attributes. var inlineTags = new RegExp(['^(<\\/?(a|abbr|acronym|applet|area|b|basefont|', 'bdo|big|button|cite|code|del|dfn|em|figcaption|', 'font|i|iframe|img|input|ins|kbd|label|map|', 'mark|meter|object|param|progress|q|ruby|rp|rt|s|', 'samp|script|select|small|span|strike|strong|', 'sub|sup|textarea|time|tt|u|var|wbr)[^>]*>|', '<(br)\\s?\\/?>)$'].join(''), 'i'); /****************************************************************** * Utility Functions * *****************************************************************/ // patch for ie7 if (!Array.indexOf) { Array.prototype.indexOf = function(obj) { for (var i = 0; i < this.length; i++) { if (this[i] == obj) { return i; } } return -1; }; } function trim(str) { return str.replace(/^\s+|\s+$/g, ''); } function rtrim(str) { return str.replace(/\s+$/g, ''); } // Remove one level of indentation from text. Indent is 4 spaces. function outdent(text) { return text.replace(new RegExp('^(\\t|[ ]{1,4})', 'gm'), ''); } function contains(str, substr) { return str.indexOf(substr) != -1; } // Sanitize html, removing tags that aren't in the whitelist function sanitizeHtml(html, whitelist) { return html.replace(/<[^>]*>?/gi, function(tag) { return tag.match(whitelist) ? tag : ''; }); } // Merge two arrays, keeping only unique elements. function union(x, y) { var obj = {}; for (var i = 0; i < x.length; i++) obj[x[i]] = x[i]; for (i = 0; i < y.length; i++) obj[y[i]] = y[i]; var res = []; for (var k in obj) { if (obj.hasOwnProperty(k)) res.push(obj[k]); } return res; } // JS regexes don't support \A or \Z, so we add sentinels, as Pagedown // does. In this case, we add the ascii codes for start of text (STX) and // end of text (ETX), an idea borrowed from: // https://github.com/tanakahisateru/js-markdown-extra function addAnchors(text) { if(text.charAt(0) != '\x02') text = '\x02' + text; if(text.charAt(text.length - 1) != '\x03') text = text + '\x03'; return text; } // Remove STX and ETX sentinels. function removeAnchors(text) { if(text.charAt(0) == '\x02') text = text.substr(1); if(text.charAt(text.length - 1) == '\x03') text = text.substr(0, text.length - 1); return text; } // Convert markdown within an element, retaining only span-level tags function convertSpans(text, extra) { return sanitizeHtml(convertAll(text, extra), inlineTags); } // Convert internal markdown using the stock pagedown converter function convertAll(text, extra) { var result = extra.blockGamutHookCallback(text); // We need to perform these operations since we skip the steps in the converter result = unescapeSpecialChars(result); result = result.replace(/~D/g, "$$").replace(/~T/g, "~"); result = extra.previousPostConversion(result); return result; } // Convert escaped special characters to HTML decimal entity codes. function processEscapes(text) { // Markdown extra adds two escapable characters, `:` and `|` // If escaped, we convert them to html entities so our // regexes don't recognize them. Markdown doesn't support escaping // the escape character, e.g. `\\`, which make this even simpler. return text.replace(/\\\|/g, '|').replace(/\\:/g, ':'); } // Duplicated from PageDown converter function unescapeSpecialChars(text) { // Swap back in all the special characters we've hidden. text = text.replace(/~E(\d+)E/g, function(wholeMatch, m1) { var charCodeToReplace = parseInt(m1); return String.fromCharCode(charCodeToReplace); }); return text; } function slugify(text) { return text.toLowerCase() .replace(/\s+/g, '-') // Replace spaces with - .replace(/[^\w\-]+/g, '') // Remove all non-word chars .replace(/\-\-+/g, '-') // Replace multiple - with single - .replace(/^-+/, '') // Trim - from start of text .replace(/-+$/, ''); // Trim - from end of text } /***************************************************************************** * Markdown.Extra * ****************************************************************************/ Markdown.Extra = function() { // For converting internal markdown (in tables for instance). // This is necessary since these methods are meant to be called as // preConversion hooks, and the Markdown converter passed to init() // won't convert any markdown contained in the html tags we return. this.converter = null; // Stores html blocks we generate in hooks so that // they're not destroyed if the user is using a sanitizing converter this.hashBlocks = []; // Stores footnotes this.footnotes = {}; this.usedFootnotes = []; // Special attribute blocks for fenced code blocks and headers enabled. this.attributeBlocks = false; // Fenced code block options this.googleCodePrettify = false; this.highlightJs = false; // Table options this.tableClass = ''; this.tabWidth = 4; }; Markdown.Extra.init = function(converter, options) { // Each call to init creates a new instance of Markdown.Extra so it's // safe to have multiple converters, with different options, on a single page var extra = new Markdown.Extra(); var postNormalizationTransformations = []; var preBlockGamutTransformations = []; var postConversionTransformations = ["unHashExtraBlocks"]; options = options || {}; options.extensions = options.extensions || ["all"]; if (contains(options.extensions, "all")) { options.extensions = ["tables", "fenced_code_gfm", "def_list", "attr_list", "footnotes"]; } if (contains(options.extensions, "attr_list")) { postNormalizationTransformations.push("hashFcbAttributeBlocks"); preBlockGamutTransformations.push("hashHeaderAttributeBlocks"); postConversionTransformations.push("applyAttributeBlocks"); extra.attributeBlocks = true; } if (contains(options.extensions, "tables")) { preBlockGamutTransformations.push("tables"); } if (contains(options.extensions, "fenced_code_gfm")) { postNormalizationTransformations.push("fencedCodeBlocks"); } if (contains(options.extensions, "def_list")) { preBlockGamutTransformations.push("definitionLists"); } if (contains(options.extensions, "footnotes")) { postNormalizationTransformations.push("stripFootnoteDefinitions"); preBlockGamutTransformations.push("doFootnotes"); postConversionTransformations.push("printFootnotes"); } converter.hooks.chain("postNormalization", function(text) { return extra.doTransform(postNormalizationTransformations, text) + '\n'; }); converter.hooks.chain("preBlockGamut", function(text, blockGamutHookCallback) { // Keep a reference to the block gamut callback to run recursively extra.blockGamutHookCallback = blockGamutHookCallback; text = processEscapes(text); return extra.doTransform(preBlockGamutTransformations, text) + '\n'; }); // Keep a reference to the hook chain running before doPostConversion to apply on hashed extra blocks extra.previousPostConversion = converter.hooks.postConversion; converter.hooks.chain("postConversion", function(text) { text = extra.doTransform(postConversionTransformations, text); // Clear state vars that may use unnecessary memory extra.hashBlocks = []; extra.footnotes = {}; extra.usedFootnotes = []; return text; }); if ("highlighter" in options) { extra.googleCodePrettify = options.highlighter === 'prettify'; extra.highlightJs = options.highlighter === 'highlight'; } if ("table_class" in options) { extra.tableClass = options.table_class; } extra.converter = converter; // Caller usually won't need this, but it's handy for testing. return extra; }; // Do transformations Markdown.Extra.prototype.doTransform = function(transformations, text) { for(var i = 0; i < transformations.length; i++) text = this[transformations[i]](text); return text; }; // Return a placeholder containing a key, which is the block's index in the // hashBlocks array. We wrap our output in a
tag here so Pagedown won't. Markdown.Extra.prototype.hashExtraBlock = function(block) { return '\n
~X' + (this.hashBlocks.push(block) - 1) + 'X
\n'; }; Markdown.Extra.prototype.hashExtraInline = function(block) { return '~X' + (this.hashBlocks.push(block) - 1) + 'X'; }; // Replace placeholder blocks in `text` with their corresponding // html blocks in the hashBlocks array. Markdown.Extra.prototype.unHashExtraBlocks = function(text) { var self = this; function recursiveUnHash() { var hasHash = false; text = text.replace(/(?:)?~X(\d+)X(?:<\/p>)?/g, function(wholeMatch, m1) { hasHash = true; var key = parseInt(m1, 10); return self.hashBlocks[key]; }); if(hasHash === true) { recursiveUnHash(); } } recursiveUnHash(); return text; }; /****************************************************************** * Attribute Blocks * *****************************************************************/ // Extract headers attribute blocks, move them above the element they will be // applied to, and hash them for later. Markdown.Extra.prototype.hashHeaderAttributeBlocks = function(text) { // TODO: use sentinels. Should we just add/remove them in doConversion? // TODO: better matches for id / class attributes var attrBlock = "\\{\\s*[.|#][^}]+\\}"; var hdrAttributesA = new RegExp("^(#{1,6}.*#{0,6})\\s+(" + attrBlock + ")[ \\t]*(\\n|0x03)", "gm"); var hdrAttributesB = new RegExp("^(.*)\\s+(" + attrBlock + ")[ \\t]*\\n" + "(?=[\\-|=]+\\s*(\\n|0x03))", "gm"); // underline lookahead var self = this; function attributeCallback(wholeMatch, pre, attr) { return '
~XX' + (self.hashBlocks.push(attr) - 1) + 'XX
\n' + pre + "\n"; } text = text.replace(hdrAttributesA, attributeCallback); // ## headers text = text.replace(hdrAttributesB, attributeCallback); // underline headers return text; }; // Extract FCB attribute blocks, move them above the element they will be // applied to, and hash them for later. Markdown.Extra.prototype.hashFcbAttributeBlocks = function(text) { // TODO: use sentinels. Should we just add/remove them in doConversion? // TODO: better matches for id / class attributes var attrBlock = "\\{\\s*[.|#][^}]+\\}"; var fcbAttributes = new RegExp("^(```[^{\\n]*)\\s+(" + attrBlock + ")[ \\t]*\\n" + "(?=([\\s\\S]*?)\\n```\\s*(\\n|0x03))", "gm"); var self = this; function attributeCallback(wholeMatch, pre, attr) { return '~XX' + (self.hashBlocks.push(attr) - 1) + 'XX
\n' + pre + "\n"; } return text.replace(fcbAttributes, attributeCallback); }; Markdown.Extra.prototype.applyAttributeBlocks = function(text) { var self = this; var blockRe = new RegExp('~XX(\\d+)XX
[\\s]*' + '(?:<(h[1-6]|pre)(?: +class="(\\S+)")?(>[\\s\\S]*?\\2>))', "gm"); text = text.replace(blockRe, function(wholeMatch, k, tag, cls, rest) { if (!tag) // no following header or fenced code block. return ''; // get attributes list from hash var key = parseInt(k, 10); var attributes = self.hashBlocks[key]; // get id var id = attributes.match(/#[^\s{}]+/g) || []; var idStr = id[0] ? ' id="' + id[0].substr(1, id[0].length - 1) + '"' : ''; // get classes and merge with existing classes var classes = attributes.match(/\.[^\s{}]+/g) || []; for (var i = 0; i < classes.length; i++) // Remove leading dot classes[i] = classes[i].substr(1, classes[i].length - 1); var classStr = ''; if (cls) classes = union(classes, [cls]); if (classes.length > 0) classStr = ' class="' + classes.join(' ') + '"'; return "<" + tag + idStr + classStr + rest; }); return text; }; /****************************************************************** * Tables * *****************************************************************/ // Find and convert Markdown Extra tables into html. Markdown.Extra.prototype.tables = function(text) { var self = this; var leadingPipe = new RegExp( ['^' , '[ ]{0,3}' , // Allowed whitespace '[|]' , // Initial pipe '(.+)\\n' , // $1: Header Row '[ ]{0,3}' , // Allowed whitespace '[|]([ ]*[-:]+[-| :]*)\\n' , // $2: Separator '(' , // $3: Table Body '(?:[ ]*[|].*\\n?)*' , // Table rows ')', '(?:\\n|$)' // Stop at final newline ].join(''), 'gm' ); var noLeadingPipe = new RegExp( ['^' , '[ ]{0,3}' , // Allowed whitespace '(\\S.*[|].*)\\n' , // $1: Header Row '[ ]{0,3}' , // Allowed whitespace '([-:]+[ ]*[|][-| :]*)\\n' , // $2: Separator '(' , // $3: Table Body '(?:.*[|].*\\n?)*' , // Table rows ')' , '(?:\\n|$)' // Stop at final newline ].join(''), 'gm' ); text = text.replace(leadingPipe, doTable); text = text.replace(noLeadingPipe, doTable); // $1 = header, $2 = separator, $3 = body function doTable(match, header, separator, body, offset, string) { // remove any leading pipes and whitespace header = header.replace(/^ *[|]/m, ''); separator = separator.replace(/^ *[|]/m, ''); body = body.replace(/^ *[|]/gm, ''); // remove trailing pipes and whitespace header = header.replace(/[|] *$/m, ''); separator = separator.replace(/[|] *$/m, ''); body = body.replace(/[|] *$/gm, ''); // determine column alignments alignspecs = separator.split(/ *[|] */); align = []; for (var i = 0; i < alignspecs.length; i++) { var spec = alignspecs[i]; if (spec.match(/^ *-+: *$/m)) align[i] = ' style="text-align:right;"'; else if (spec.match(/^ *:-+: *$/m)) align[i] = ' style="text-align:center;"'; else if (spec.match(/^ *:-+ *$/m)) align[i] = ' style="text-align:left;"'; else align[i] = ''; } // TODO: parse spans in header and rows before splitting, so that pipes // inside of tags are not interpreted as separators var headers = header.split(/ *[|] */); var colCount = headers.length; // build html var cls = self.tableClass ? ' class="' + self.tableClass + '"' : ''; var html = ['", headerHtml, " | \n"].join(''); } html += "
---|
", colHtml, " | \n"].join(''); } html += "
',
encodeCode(codeblock), '
'].join('');
// replace codeblock with placeholder until postConversion step
return self.hashExtraBlock(html);
});
return text;
};
/******************************************************************
* Definition Lists *
******************************************************************/
// Find and convert markdown extra definition lists into html.
Markdown.Extra.prototype.definitionLists = function(text) {
var wholeList = new RegExp(
['(\\x02\\n?|\\n\\n)' ,
'(?:' ,
'(' , // $1 = whole list
'(' , // $2
'[ ]{0,3}' ,
'((?:[ \\t]*\\S.*\\n)+)', // $3 = defined term
'\\n?' ,
'[ ]{0,3}:[ ]+' , // colon starting definition
')' ,
'([\\s\\S]+?)' ,
'(' , // $4
'(?=\\0x03)' , // \z
'|' ,
'(?=' ,
'\\n{2,}' ,
'(?=\\S)' ,
'(?!' , // Negative lookahead for another term
'[ ]{0,3}' ,
'(?:\\S.*\\n)+?' , // defined term
'\\n?' ,
'[ ]{0,3}:[ ]+' , // colon starting definition
')' ,
'(?!' , // Negative lookahead for another definition
'[ ]{0,3}:[ ]+' , // colon starting definition
')' ,
')' ,
')' ,
')' ,
')'
].join(''),
'gm'
);
var self = this;
text = addAnchors(text);
text = text.replace(wholeList, function(match, pre, list) {
var result = trim(self.processDefListItems(list));
result = "