vendor/assets/javascripts/markdown.extra.js in pagedown-bootstrap-rails-2.0.0 vs vendor/assets/javascripts/markdown.extra.js in pagedown-bootstrap-rails-2.1.0

- old
+ new

@@ -1,706 +1,874 @@ (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'); + // 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 * - *****************************************************************/ + /****************************************************************** + * 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; - }; - } + // 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 trim(str) { + return str.replace(/^\s+|\s+$/g, ''); + } - function rtrim(str) { - return str.replace(/\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'), ''); - } + // 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; - } + 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 : ''; - }); - } + // 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]); + // 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; } - 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; - } + // 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; - } + // 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 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 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, '&#124;').replace(/\\:/g, '&#58;'); - } + // Convert escaped special characters + function processEscapesStep1(text) { + // Markdown extra adds two escapable characters, `:` and `|` + return text.replace(/\\\|/g, '~I').replace(/\\:/g, '~i'); + } + function processEscapesStep2(text) { + return text.replace(/~I/g, '|').replace(/~i/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; - } + // 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 - } + 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 * + ****************************************************************************/ - 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; + 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 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 = []; + // Stores footnotes + this.footnotes = {}; + this.usedFootnotes = []; - // Special attribute blocks for fenced code blocks and headers enabled. - this.attributeBlocks = false; + // Special attribute blocks for fenced code blocks and headers enabled. + this.attributeBlocks = false; - // Fenced code block options - this.googleCodePrettify = false; - this.highlightJs = false; + // Fenced code block options + this.googleCodePrettify = false; + this.highlightJs = false; - // Table options - this.tableClass = ''; + // Table options + this.tableClass = ''; - this.tabWidth = 4; - }; + 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"]; + 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 postSpanGamutTransformations = []; + 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"); - } + options = options || {}; + options.extensions = options.extensions || ["all"]; + if (contains(options.extensions, "all")) { + options.extensions = ["tables", "fenced_code_gfm", "def_list", "attr_list", "footnotes", "smartypants", "strikethrough", "newlines"]; + } + preBlockGamutTransformations.push("wrapHeaders"); + if (contains(options.extensions, "attr_list")) { + postNormalizationTransformations.push("hashFcbAttributeBlocks"); + preBlockGamutTransformations.push("hashHeaderAttributeBlocks"); + postConversionTransformations.push("applyAttributeBlocks"); + extra.attributeBlocks = true; + } + if (contains(options.extensions, "fenced_code_gfm")) { + // This step will convert fcb inside list items and blockquotes + preBlockGamutTransformations.push("fencedCodeBlocks"); + // This extra step is to prevent html blocks hashing and link definition/footnotes stripping inside fcb + postNormalizationTransformations.push("fencedCodeBlocks"); + } + if (contains(options.extensions, "tables")) { + preBlockGamutTransformations.push("tables"); + } + if (contains(options.extensions, "def_list")) { + preBlockGamutTransformations.push("definitionLists"); + } + if (contains(options.extensions, "footnotes")) { + postNormalizationTransformations.push("stripFootnoteDefinitions"); + preBlockGamutTransformations.push("doFootnotes"); + postConversionTransformations.push("printFootnotes"); + } + if (contains(options.extensions, "smartypants")) { + postConversionTransformations.push("runSmartyPants"); + } + if (contains(options.extensions, "strikethrough")) { + postSpanGamutTransformations.push("strikethrough"); + } + if (contains(options.extensions, "newlines")) { + postSpanGamutTransformations.push("newlines"); + } - converter.hooks.chain("postNormalization", function(text) { - return extra.doTransform(postNormalizationTransformations, text) + '\n'; - }); + 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'; - }); + converter.hooks.chain("preBlockGamut", function(text, blockGamutHookCallback) { + // Keep a reference to the block gamut callback to run recursively + extra.blockGamutHookCallback = blockGamutHookCallback; + text = processEscapesStep1(text); + text = extra.doTransform(preBlockGamutTransformations, text) + '\n'; + text = processEscapesStep2(text); + return text; + }); - // 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; - }); + converter.hooks.chain("postSpanGamut", function(text) { + return extra.doTransform(postSpanGamutTransformations, text); + }); - if ("highlighter" in options) { - extra.googleCodePrettify = options.highlighter === 'prettify'; - extra.highlightJs = options.highlighter === 'highlight'; - } + // 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 ("table_class" in options) { - extra.tableClass = options.table_class; - } + if ("highlighter" in options) { + extra.googleCodePrettify = options.highlighter === 'prettify'; + extra.highlightJs = options.highlighter === 'highlight'; + } - extra.converter = converter; + if ("table_class" in options) { + extra.tableClass = options.table_class; + } - // Caller usually won't need this, but it's handy for testing. - return extra; - }; + extra.converter = converter; - // Do transformations - Markdown.Extra.prototype.doTransform = function(transformations, text) { - for(var i = 0; i < transformations.length; i++) - text = this[transformations[i]](text); - return text; - }; + // Caller usually won't need this, but it's handy for testing. + return extra; + }; - // Return a placeholder containing a key, which is the block's index in the - // hashBlocks array. We wrap our output in a <p> tag here so Pagedown won't. - Markdown.Extra.prototype.hashExtraBlock = function(block) { - return '\n<p>~X' + (this.hashBlocks.push(block) - 1) + 'X</p>\n'; - }; - Markdown.Extra.prototype.hashExtraInline = function(block) { - return '~X' + (this.hashBlocks.push(block) - 1) + 'X'; - }; + // Do transformations + Markdown.Extra.prototype.doTransform = function(transformations, text) { + for(var i = 0; i < transformations.length; i++) + text = this[transformations[i]](text); + return text; + }; - // 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(/(?:<p>)?~X(\d+)X(?:<\/p>)?/g, function(wholeMatch, m1) { - hasHash = true; - var key = parseInt(m1, 10); - return self.hashBlocks[key]; - }); - if(hasHash === true) { + // Return a placeholder containing a key, which is the block's index in the + // hashBlocks array. We wrap our output in a <p> tag here so Pagedown won't. + Markdown.Extra.prototype.hashExtraBlock = function(block) { + return '\n<p>~X' + (this.hashBlocks.push(block) - 1) + 'X</p>\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(/(?:<p>)?~X(\d+)X(?:<\/p>)?/g, function(wholeMatch, m1) { + hasHash = true; + var key = parseInt(m1, 10); + return self.hashBlocks[key]; + }); + if(hasHash === true) { + recursiveUnHash(); + } + } recursiveUnHash(); - } - } - recursiveUnHash(); - return text; - }; + return text; + }; + // Wrap headers to make sure they won't be in def lists + Markdown.Extra.prototype.wrapHeaders = function(text) { + function wrap(text) { + return '\n' + text + '\n'; + } + text = text.replace(/^.+[ \t]*\n=+[ \t]*\n+/gm, wrap); + text = text.replace(/^.+[ \t]*\n-+[ \t]*\n+/gm, wrap); + text = text.replace(/^\#{1,6}[ \t]*.+?[ \t]*\#*\n+/gm, wrap); + 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) { + /****************************************************************** + * Attribute Blocks * + *****************************************************************/ + // 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 attrBlock = "\\{[ \\t]*((?:[#.][-_:a-zA-Z0-9]+[ \\t]*)+)\\}"; + var hdrAttributesA = new RegExp("^(#{1,6}.*#{0,6})[ \\t]+" + attrBlock + "[ \\t]*(?:\\n|0x03)", "gm"); + var hdrAttributesB = new RegExp("^(.*)[ \\t]+" + attrBlock + "[ \\t]*\\n" + + "(?=[\\-|=]+\\s*(?:\\n|0x03))", "gm"); // underline lookahead + var fcbAttributes = new RegExp("^(```[ \\t]*[^{\\s]*)[ \\t]+" + attrBlock + "[ \\t]*\\n" + + "(?=([\\s\\S]*?)\\n```[ \\t]*(\\n|0x03))", "gm"); - var self = this; - function attributeCallback(wholeMatch, pre, attr) { - return '<p>~XX' + (self.hashBlocks.push(attr) - 1) + 'XX</p>\n' + pre + "\n"; - } + // 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) { - text = text.replace(hdrAttributesA, attributeCallback); // ## headers - text = text.replace(hdrAttributesB, attributeCallback); // underline headers - return text; - }; + var self = this; + function attributeCallback(wholeMatch, pre, attr) { + return '<p>~XX' + (self.hashBlocks.push(attr) - 1) + 'XX</p>\n' + pre + "\n"; + } - // 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"); + text = text.replace(hdrAttributesA, attributeCallback); // ## headers + text = text.replace(hdrAttributesB, attributeCallback); // underline headers + return text; + }; - var self = this; - function attributeCallback(wholeMatch, pre, attr) { - return '<p>~XX' + (self.hashBlocks.push(attr) - 1) + 'XX</p>\n' + pre + "\n"; - } + // 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 - return text.replace(fcbAttributes, attributeCallback); - }; + var self = this; + function attributeCallback(wholeMatch, pre, attr) { + return '<p>~XX' + (self.hashBlocks.push(attr) - 1) + 'XX</p>\n' + pre + "\n"; + } - Markdown.Extra.prototype.applyAttributeBlocks = function(text) { - var self = this; - var blockRe = new RegExp('<p>~XX(\\d+)XX</p>[\\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 ''; + return text.replace(fcbAttributes, attributeCallback); + }; - // get attributes list from hash - var key = parseInt(k, 10); - var attributes = self.hashBlocks[key]; + Markdown.Extra.prototype.applyAttributeBlocks = function(text) { + var self = this; + var blockRe = new RegExp('<p>~XX(\\d+)XX</p>[\\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 id - var id = attributes.match(/#[^\s{}]+/g) || []; - var idStr = id[0] ? ' id="' + id[0].substr(1, id[0].length - 1) + '"' : ''; + // get attributes list from hash + var key = parseInt(k, 10); + var attributes = self.hashBlocks[key]; - // 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); + // get id + var id = attributes.match(/#[^\s#.]+/g) || []; + var idStr = id[0] ? ' id="' + id[0].substr(1, id[0].length - 1) + '"' : ''; - var classStr = ''; - if (cls) - classes = union(classes, [cls]); + // 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); - if (classes.length > 0) - classStr = ' class="' + classes.join(' ') + '"'; + var classStr = ''; + if (cls) + classes = union(classes, [cls]); - return "<" + tag + idStr + classStr + rest; - }); + if (classes.length > 0) + classStr = ' class="' + classes.join(' ') + '"'; - return text; - }; + return "<" + tag + idStr + classStr + rest; + }); - /****************************************************************** - * Tables * - *****************************************************************/ + return text; + }; - // Find and convert Markdown Extra tables into html. - Markdown.Extra.prototype.tables = function(text) { - var self = this; + /****************************************************************** + * Tables * + *****************************************************************/ - var leadingPipe = new RegExp( - ['^' , - '[ ]{0,3}' , // Allowed whitespace - '[|]' , // Initial pipe - '(.+)\\n' , // $1: Header Row + // Find and convert Markdown Extra tables into html. + Markdown.Extra.prototype.tables = function(text) { - '[ ]{0,3}' , // Allowed whitespace - '[|]([ ]*[-:]+[-| :]*)\\n' , // $2: Separator + var self = this; - '(' , // $3: Table Body - '(?:[ ]*[|].*\\n?)*' , // Table rows - ')', - '(?:\\n|$)' // Stop at final newline - ].join(''), - 'gm' - ); + var leadingPipe = new RegExp( + ['^' , + '[ ]{0,3}' , // Allowed whitespace + '[|]' , // Initial pipe + '(.+)\\n' , // $1: Header Row - var noLeadingPipe = new RegExp( - ['^' , - '[ ]{0,3}' , // Allowed whitespace - '(\\S.*[|].*)\\n' , // $1: Header Row + '[ ]{0,3}' , // Allowed whitespace + '[|]([ ]*[-:]+[-| :]*)\\n' , // $2: Separator - '[ ]{0,3}' , // Allowed whitespace - '([-:]+[ ]*[|][-| :]*)\\n' , // $2: Separator + '(' , // $3: Table Body + '(?:[ ]*[|].*\\n?)*' , // Table rows + ')', + '(?:\\n|$)' // Stop at final newline + ].join(''), + 'gm' + ); - '(' , // $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 - text = text.replace(leadingPipe, doTable); - text = text.replace(noLeadingPipe, doTable); + '[ ]{0,3}' , // Allowed whitespace + '([-:]+[ ]*[|][-| :]*)\\n' , // $2: Separator - // $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, ''); + '(' , // $3: Table Body + '(?:.*[|].*\\n?)*' , // Table rows + ')' , + '(?:\\n|$)' // Stop at final newline + ].join(''), + 'gm' + ); - // remove trailing pipes and whitespace - header = header.replace(/[|] *$/m, ''); - separator = separator.replace(/[|] *$/m, ''); - body = body.replace(/[|] *$/gm, ''); + text = text.replace(leadingPipe, doTable); + text = text.replace(noLeadingPipe, doTable); - // 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] = ''; - } + // $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, ''); - // 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; + // remove trailing pipes and whitespace + header = header.replace(/[|] *$/m, ''); + separator = separator.replace(/[|] *$/m, ''); + body = body.replace(/[|] *$/gm, ''); - // build html - var cls = self.tableClass ? ' class="' + self.tableClass + '"' : ''; - var html = ['<table', cls, '>\n', '<thead>\n', '<tr>\n'].join(''); + // 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] = ''; + } - // build column headers. - for (i = 0; i < colCount; i++) { - var headerHtml = convertSpans(trim(headers[i]), self); - html += [" <th", align[i], ">", headerHtml, "</th>\n"].join(''); - } - html += "</tr>\n</thead>\n"; + // 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 rows - var rows = body.split('\n'); - for (i = 0; i < rows.length; i++) { - if (rows[i].match(/^\s*$/)) // can apply to final row - continue; + // build html + var cls = self.tableClass ? ' class="' + self.tableClass + '"' : ''; + var html = ['<table', cls, '>\n', '<thead>\n', '<tr>\n'].join(''); - // ensure number of rowCells matches colCount - var rowCells = rows[i].split(/ *[|] */); - var lenDiff = colCount - rowCells.length; - for (var j = 0; j < lenDiff; j++) - rowCells.push(''); + // build column headers. + for (i = 0; i < colCount; i++) { + var headerHtml = convertSpans(trim(headers[i]), self); + html += [" <th", align[i], ">", headerHtml, "</th>\n"].join(''); + } + html += "</tr>\n</thead>\n"; - html += "<tr>\n"; - for (j = 0; j < colCount; j++) { - var colHtml = convertSpans(trim(rowCells[j]), self); - html += [" <td", align[j], ">", colHtml, "</td>\n"].join(''); + // build rows + var rows = body.split('\n'); + for (i = 0; i < rows.length; i++) { + if (rows[i].match(/^\s*$/)) // can apply to final row + continue; + + // ensure number of rowCells matches colCount + var rowCells = rows[i].split(/ *[|] */); + var lenDiff = colCount - rowCells.length; + for (var j = 0; j < lenDiff; j++) + rowCells.push(''); + + html += "<tr>\n"; + for (j = 0; j < colCount; j++) { + var colHtml = convertSpans(trim(rowCells[j]), self); + html += [" <td", align[j], ">", colHtml, "</td>\n"].join(''); + } + html += "</tr>\n"; + } + + html += "</table>\n"; + + // replace html with placeholder until postConversion step + return self.hashExtraBlock(html); } - html += "</tr>\n"; - } - html += "</table>\n"; + return text; + }; - // replace html with placeholder until postConversion step - return self.hashExtraBlock(html); - } - return text; - }; + /****************************************************************** + * Footnotes * + *****************************************************************/ + // Strip footnote, store in hashes. + Markdown.Extra.prototype.stripFootnoteDefinitions = function(text) { + var self = this; - /****************************************************************** - * Footnotes * - *****************************************************************/ + text = text.replace( + /\n[ ]{0,3}\[\^(.+?)\]\:[ \t]*\n?([\s\S]*?)\n{1,2}((?=\n[ ]{0,3}\S)|$)/g, + function(wholeMatch, m1, m2) { + m1 = slugify(m1); + m2 += "\n"; + m2 = m2.replace(/^[ ]{0,3}/g, ""); + self.footnotes[m1] = m2; + return "\n"; + }); - // Strip footnote, store in hashes. - Markdown.Extra.prototype.stripFootnoteDefinitions = function(text) { - var self = this; + return text; + }; - text = text.replace( - /\n[ ]{0,3}\[\^(.+?)\]\:[ \t]*\n?([\s\S]*?)\n{1,2}((?=\n[ ]{0,3}\S)|$)/g, - function(wholeMatch, m1, m2) { - m1 = slugify(m1); - m2 += "\n"; - m2 = m2.replace(/^[ ]{0,3}/g, ""); - self.footnotes[m1] = m2; - return "\n"; - }); - return text; - }; + // Find and convert footnotes references. + Markdown.Extra.prototype.doFootnotes = function(text) { + var self = this; + if(self.isConvertingFootnote === true) { + return text; + } + var footnoteCounter = 0; + text = text.replace(/\[\^(.+?)\]/g, function(wholeMatch, m1) { + var id = slugify(m1); + var footnote = self.footnotes[id]; + if (footnote === undefined) { + return wholeMatch; + } + footnoteCounter++; + self.usedFootnotes.push(id); + var html = '<a href="#fn:' + id + '" id="fnref:' + id + + '" title="See footnote" class="footnote">' + footnoteCounter + + '</a>'; + return self.hashExtraInline(html); + }); - // Find and convert footnotes references. - Markdown.Extra.prototype.doFootnotes = function(text) { - var self = this; - if(self.isConvertingFootnote === true) { - return text; - } + return text; + }; - var footnoteCounter = 0; - text = text.replace(/\[\^(.+?)\]/g, function(wholeMatch, m1) { - var id = slugify(m1); - var footnote = self.footnotes[id]; - if (footnote === undefined) { - return ""; - } - footnoteCounter++; - self.usedFootnotes.push(id); - var html = '<a href="#fn:' + id + '" id="fnref:' + id - + '" title="See footnote" class="footnote">' + footnoteCounter - + '</a>'; - return self.hashExtraInline(html); - }); + // Print footnotes at the end of the document + Markdown.Extra.prototype.printFootnotes = function(text) { + var self = this; - return text; - }; + if (self.usedFootnotes.length === 0) { + return text; + } - // Print footnotes at the end of the document - Markdown.Extra.prototype.printFootnotes = function(text) { - var self = this; + text += '\n\n<div class="footnotes">\n<hr>\n<ol>\n\n'; + for(var i=0; i<self.usedFootnotes.length; i++) { + var id = self.usedFootnotes[i]; + var footnote = self.footnotes[id]; + self.isConvertingFootnote = true; + var formattedfootnote = convertSpans(footnote, self); + delete self.isConvertingFootnote; + text += '<li id="fn:' + + id + + '">' + + formattedfootnote + + ' <a href="#fnref:' + + id + + '" title="Return to article" class="reversefootnote">&#8617;</a></li>\n\n'; + } + text += '</ol>\n</div>'; + return text; + }; - if (self.usedFootnotes.length === 0) { - return text; - } - text += '\n\n<div class="footnotes">\n<hr>\n<ol>\n\n'; - for(var i=0; i<self.usedFootnotes.length; i++) { - var id = self.usedFootnotes[i]; - var footnote = self.footnotes[id]; - self.isConvertingFootnote = true; - var formattedfootnote = convertSpans(footnote, self); - delete self.isConvertingFootnote; - text += '<li id="fn:' - + id - + '">' - + formattedfootnote - + ' <a href="#fnref:' - + id - + '" title="Return to article" class="reversefootnote">&#8617;</a></li>\n\n'; - } - text += '</ol>\n</div>'; - return text; - }; + /****************************************************************** + * Fenced Code Blocks (gfm) * + ******************************************************************/ + // Find and convert gfm-inspired fenced code blocks into html. + Markdown.Extra.prototype.fencedCodeBlocks = function(text) { + function encodeCode(code) { + code = code.replace(/&/g, "&amp;"); + code = code.replace(/</g, "&lt;"); + code = code.replace(/>/g, "&gt;"); + // These were escaped by PageDown before postNormalization + code = code.replace(/~D/g, "$$"); + code = code.replace(/~T/g, "~"); + return code; + } - /****************************************************************** - * Fenced Code Blocks (gfm) * - ******************************************************************/ + var self = this; + text = text.replace(/(?:^|\n)```[ \t]*(\S*)[ \t]*\n([\s\S]*?)\n```[ \t]*(?=\n)/g, function(match, m1, m2) { + var language = m1, codeblock = m2; - // Find and convert gfm-inspired fenced code blocks into html. - Markdown.Extra.prototype.fencedCodeBlocks = function(text) { - function encodeCode(code) { - code = code.replace(/&/g, "&amp;"); - code = code.replace(/</g, "&lt;"); - code = code.replace(/>/g, "&gt;"); - // These were escaped by PageDown before postNormalization - code = code.replace(/~D/g, "$$"); - code = code.replace(/~T/g, "~"); - return code; + // adhere to specified options + var preclass = self.googleCodePrettify ? ' class="prettyprint"' : ''; + var codeclass = ''; + if (language) { + if (self.googleCodePrettify || self.highlightJs) { + // use html5 language- class names. supported by both prettify and highlight.js + codeclass = ' class="language-' + language + '"'; + } else { + codeclass = ' class="' + language + '"'; + } + } + + var html = ['<pre', preclass, '><code', codeclass, '>', + encodeCode(codeblock), '</code></pre>'].join(''); + + // replace codeblock with placeholder until postConversion step + return self.hashExtraBlock(html); + }); + + return text; + }; + + + /****************************************************************** + * SmartyPants * + ******************************************************************/ + + Markdown.Extra.prototype.educatePants = function(text) { + var self = this; + var result = ''; + var blockOffset = 0; + // Here we parse HTML in a very bad manner + text.replace(/(?:<!--[\s\S]*?-->)|(<)([a-zA-Z1-6]+)([^\n]*?>)([\s\S]*?)(<\/\2>)/g, function(wholeMatch, m1, m2, m3, m4, m5, offset) { + var token = text.substring(blockOffset, offset); + result += self.applyPants(token); + self.smartyPantsLastChar = result.substring(result.length - 1); + blockOffset = offset + wholeMatch.length; + if(!m1) { + // Skip commentary + result += wholeMatch; + return; + } + // Skip special tags + if(!/code|kbd|pre|script|noscript|iframe|math|ins|del|pre/i.test(m2)) { + m4 = self.educatePants(m4); + } + else { + self.smartyPantsLastChar = m4.substring(m4.length - 1); + } + result += m1 + m2 + m3 + m4 + m5; + }); + var lastToken = text.substring(blockOffset); + result += self.applyPants(lastToken); + self.smartyPantsLastChar = result.substring(result.length - 1); + return result; + }; + + function revertPants(wholeMatch, m1) { + var blockText = m1; + blockText = blockText.replace(/&\#8220;/g, "\""); + blockText = blockText.replace(/&\#8221;/g, "\""); + blockText = blockText.replace(/&\#8216;/g, "'"); + blockText = blockText.replace(/&\#8217;/g, "'"); + blockText = blockText.replace(/&\#8212;/g, "---"); + blockText = blockText.replace(/&\#8211;/g, "--"); + blockText = blockText.replace(/&\#8230;/g, "..."); + return blockText; } - var self = this; - text = text.replace(/(?:^|\n)```(.*)\n([\s\S]*?)\n```/g, function(match, m1, m2) { - var language = m1, codeblock = m2; + Markdown.Extra.prototype.applyPants = function(text) { + // Dashes + text = text.replace(/---/g, "&#8212;").replace(/--/g, "&#8211;"); + // Ellipses + text = text.replace(/\.\.\./g, "&#8230;").replace(/\.\s\.\s\./g, "&#8230;"); + // Backticks + text = text.replace(/``/g, "&#8220;").replace (/''/g, "&#8221;"); - // adhere to specified options - var preclass = self.googleCodePrettify ? ' class="prettyprint"' : ''; - var codeclass = ''; - if (language) { - if (self.googleCodePrettify || self.highlightJs) { - // use html5 language- class names. supported by both prettify and highlight.js - codeclass = ' class="language-' + language + '"'; - } else { - codeclass = ' class="' + language + '"'; + if(/^'$/.test(text)) { + // Special case: single-character ' token + if(/\S/.test(this.smartyPantsLastChar)) { + return "&#8217;"; + } + return "&#8216;"; } - } + if(/^"$/.test(text)) { + // Special case: single-character " token + if(/\S/.test(this.smartyPantsLastChar)) { + return "&#8221;"; + } + return "&#8220;"; + } - var html = ['<pre', preclass, '><code', codeclass, '>', - encodeCode(codeblock), '</code></pre>'].join(''); + // Special case if the very first character is a quote + // followed by punctuation at a non-word-break. Close the quotes by brute force: + text = text.replace (/^'(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "&#8217;"); + text = text.replace (/^"(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "&#8221;"); - // replace codeblock with placeholder until postConversion step - return self.hashExtraBlock(html); - }); + // Special case for double sets of quotes, e.g.: + // <p>He said, "'Quoted' words in a larger quote."</p> + text = text.replace(/"'(?=\w)/g, "&#8220;&#8216;"); + text = text.replace(/'"(?=\w)/g, "&#8216;&#8220;"); - return text; - }; + // Special case for decade abbreviations (the '80s): + text = text.replace(/'(?=\d{2}s)/g, "&#8217;"); + // Get most opening single quotes: + text = text.replace(/(\s|&nbsp;|--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)'(?=\w)/g, "$1&#8216;"); - /****************************************************************** - * Definition Lists * - ******************************************************************/ + // Single closing quotes: + text = text.replace(/([^\s\[\{\(\-])'/g, "$1&#8217;"); + text = text.replace(/'(?=\s|s\b)/g, "&#8217;"); - // 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' - ); + // Any remaining single quotes should be opening ones: + text = text.replace(/'/g, "&#8216;"); - var self = this; - text = addAnchors(text); + // Get most opening double quotes: + text = text.replace(/(\s|&nbsp;|--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)"(?=\w)/g, "$1&#8220;"); - text = text.replace(wholeList, function(match, pre, list) { - var result = trim(self.processDefListItems(list)); - result = "<dl>\n" + result + "\n</dl>"; - return pre + self.hashExtraBlock(result) + "\n\n"; - }); + // Double closing quotes: + text = text.replace(/([^\s\[\{\(\-])"/g, "$1&#8221;"); + text = text.replace(/"(?=\s)/g, "&#8221;"); - return removeAnchors(text); - }; + // Any remaining quotes should be opening ones. + text = text.replace(/"/ig, "&#8220;"); + return text; + }; - // Process the contents of a single definition list, splitting it - // into individual term and definition list items. - Markdown.Extra.prototype.processDefListItems = function(listStr) { - var self = this; + // Find and convert markdown extra definition lists into html. + Markdown.Extra.prototype.runSmartyPants = function(text) { + this.smartyPantsLastChar = ''; + text = this.educatePants(text); + // Clean everything inside html tags (some of them may have been converted due to our rough html parsing) + text = text.replace(/(<([a-zA-Z1-6]+)\b([^\n>]*?)(\/)?>)/g, revertPants); + return text; + }; - var dt = new RegExp( - ['(\\x02\\n?|\\n\\n+)' , // leading line - '(' , // definition terms = $1 - '[ ]{0,3}' , // leading whitespace - '(?![:][ ]|[ ])' , // negative lookahead for a definition - // mark (colon) or more whitespace - '(?:\\S.*\\n)+?' , // actual term (not whitespace) - ')' , - '(?=\\n?[ ]{0,3}:[ ])' // lookahead for following line feed - ].join(''), // with a definition mark - 'gm' - ); + /****************************************************************** + * Definition Lists * + ******************************************************************/ - var dd = new RegExp( - ['\\n(\\n+)?' , // leading line = $1 - '(' , // marker space = $2 - '[ ]{0,3}' , // whitespace before colon - '[:][ ]+' , // definition mark (colon) - ')' , - '([\\s\\S]+?)' , // definition text = $3 - '(?=\\n*' , // stop at next definition mark, - '(?:' , // next term or end of text - '\\n[ ]{0,3}[:][ ]|' , - '<dt>|\\x03' , // \z - ')' , - ')' - ].join(''), - 'gm' - ); + // 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' + ); - listStr = addAnchors(listStr); - // trim trailing blank lines: - listStr = listStr.replace(/\n{2,}(?=\\x03)/, "\n"); + var self = this; + text = addAnchors(text); - // Process definition terms. - listStr = listStr.replace(dt, function(match, pre, termsStr) { - var terms = trim(termsStr).split("\n"); - var text = ''; - for (var i = 0; i < terms.length; i++) { - var term = terms[i]; - // process spans inside dt - term = convertSpans(trim(term), self); - text += "\n<dt>" + term + "</dt>"; - } - return text + "\n"; - }); + text = text.replace(wholeList, function(match, pre, list) { + var result = trim(self.processDefListItems(list)); + result = "<dl>\n" + result + "\n</dl>"; + return pre + self.hashExtraBlock(result) + "\n\n"; + }); - // Process actual definitions. - listStr = listStr.replace(dd, function(match, leadingLine, markerSpace, def) { - if (leadingLine || def.match(/\n{2,}/)) { - // replace marker with the appropriate whitespace indentation - def = Array(markerSpace.length + 1).join(' ') + def; - // process markdown inside definition - // TODO?: currently doesn't apply extensions - def = outdent(def) + "\n\n"; - def = "\n" + convertAll(def, self) + "\n"; - } else { - // convert span-level markdown inside definition - def = rtrim(def); - def = convertSpans(outdent(def), self); - } + return removeAnchors(text); + }; - return "\n<dd>" + def + "</dd>\n"; - }); + // Process the contents of a single definition list, splitting it + // into individual term and definition list items. + Markdown.Extra.prototype.processDefListItems = function(listStr) { + var self = this; - return removeAnchors(listStr); - }; + var dt = new RegExp( + ['(\\x02\\n?|\\n\\n+)' , // leading line + '(' , // definition terms = $1 + '[ ]{0,3}' , // leading whitespace + '(?![:][ ]|[ ])' , // negative lookahead for a definition + // mark (colon) or more whitespace + '(?:\\S.*\\n)+?' , // actual term (not whitespace) + ')' , + '(?=\\n?[ ]{0,3}:[ ])' // lookahead for following line feed + ].join(''), // with a definition mark + 'gm' + ); + + var dd = new RegExp( + ['\\n(\\n+)?' , // leading line = $1 + '(' , // marker space = $2 + '[ ]{0,3}' , // whitespace before colon + '[:][ ]+' , // definition mark (colon) + ')' , + '([\\s\\S]+?)' , // definition text = $3 + '(?=\\n*' , // stop at next definition mark, + '(?:' , // next term or end of text + '\\n[ ]{0,3}[:][ ]|' , + '<dt>|\\x03' , // \z + ')' , + ')' + ].join(''), + 'gm' + ); + + listStr = addAnchors(listStr); + // trim trailing blank lines: + listStr = listStr.replace(/\n{2,}(?=\\x03)/, "\n"); + + // Process definition terms. + listStr = listStr.replace(dt, function(match, pre, termsStr) { + var terms = trim(termsStr).split("\n"); + var text = ''; + for (var i = 0; i < terms.length; i++) { + var term = terms[i]; + // process spans inside dt + term = convertSpans(trim(term), self); + text += "\n<dt>" + term + "</dt>"; + } + return text + "\n"; + }); + + // Process actual definitions. + listStr = listStr.replace(dd, function(match, leadingLine, markerSpace, def) { + if (leadingLine || def.match(/\n{2,}/)) { + // replace marker with the appropriate whitespace indentation + def = Array(markerSpace.length + 1).join(' ') + def; + // process markdown inside definition + // TODO?: currently doesn't apply extensions + def = outdent(def) + "\n\n"; + def = "\n" + convertAll(def, self) + "\n"; + } else { + // convert span-level markdown inside definition + def = rtrim(def); + def = convertSpans(outdent(def), self); + } + + return "\n<dd>" + def + "</dd>\n"; + }); + + return removeAnchors(listStr); + }; + + + /*********************************************************** + * Strikethrough * + ************************************************************/ + + Markdown.Extra.prototype.strikethrough = function(text) { + // Pretty much duplicated from _DoItalicsAndBold + return text.replace(/([\W_]|^)~T~T(?=\S)([^\r]*?\S[\*_]*)~T~T([\W_]|$)/g, + "$1<del>$2</del>$3"); + }; + + + /*********************************************************** + * New lines * + ************************************************************/ + + Markdown.Extra.prototype.newlines = function(text) { + // We have to ignore already converted newlines and line breaks in sub-list items + return text.replace(/(<(?:br|\/li)>)?\n/g, function(wholeMatch, previousTag) { + return previousTag ? wholeMatch : " <br>\n"; + }); + }; })();