Markdown.Extra.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874
  1. (function () {
  2. // A quick way to make sure we're only keeping span-level tags when we need to.
  3. // This isn't supposed to be foolproof. It's just a quick way to make sure we
  4. // keep all span-level tags returned by a pagedown converter. It should allow
  5. // all span-level tags through, with or without attributes.
  6. var inlineTags = new RegExp(['^(<\\/?(a|abbr|acronym|applet|area|b|basefont|',
  7. 'bdo|big|button|cite|code|del|dfn|em|figcaption|',
  8. 'font|i|iframe|img|input|ins|kbd|label|map|',
  9. 'mark|meter|object|param|progress|q|ruby|rp|rt|s|',
  10. 'samp|script|select|small|span|strike|strong|',
  11. 'sub|sup|textarea|time|tt|u|var|wbr)[^>]*>|',
  12. '<(br)\\s?\\/?>)$'].join(''), 'i');
  13. /******************************************************************
  14. * Utility Functions *
  15. *****************************************************************/
  16. // patch for ie7
  17. if (!Array.indexOf) {
  18. Array.prototype.indexOf = function(obj) {
  19. for (var i = 0; i < this.length; i++) {
  20. if (this[i] == obj) {
  21. return i;
  22. }
  23. }
  24. return -1;
  25. };
  26. }
  27. function trim(str) {
  28. return str.replace(/^\s+|\s+$/g, '');
  29. }
  30. function rtrim(str) {
  31. return str.replace(/\s+$/g, '');
  32. }
  33. // Remove one level of indentation from text. Indent is 4 spaces.
  34. function outdent(text) {
  35. return text.replace(new RegExp('^(\\t|[ ]{1,4})', 'gm'), '');
  36. }
  37. function contains(str, substr) {
  38. return str.indexOf(substr) != -1;
  39. }
  40. // Sanitize html, removing tags that aren't in the whitelist
  41. function sanitizeHtml(html, whitelist) {
  42. return html.replace(/<[^>]*>?/gi, function(tag) {
  43. return tag.match(whitelist) ? tag : '';
  44. });
  45. }
  46. // Merge two arrays, keeping only unique elements.
  47. function union(x, y) {
  48. var obj = {};
  49. for (var i = 0; i < x.length; i++)
  50. obj[x[i]] = x[i];
  51. for (i = 0; i < y.length; i++)
  52. obj[y[i]] = y[i];
  53. var res = [];
  54. for (var k in obj) {
  55. if (obj.hasOwnProperty(k))
  56. res.push(obj[k]);
  57. }
  58. return res;
  59. }
  60. // JS regexes don't support \A or \Z, so we add sentinels, as Pagedown
  61. // does. In this case, we add the ascii codes for start of text (STX) and
  62. // end of text (ETX), an idea borrowed from:
  63. // https://github.com/tanakahisateru/js-markdown-extra
  64. function addAnchors(text) {
  65. if(text.charAt(0) != '\x02')
  66. text = '\x02' + text;
  67. if(text.charAt(text.length - 1) != '\x03')
  68. text = text + '\x03';
  69. return text;
  70. }
  71. // Remove STX and ETX sentinels.
  72. function removeAnchors(text) {
  73. if(text.charAt(0) == '\x02')
  74. text = text.substr(1);
  75. if(text.charAt(text.length - 1) == '\x03')
  76. text = text.substr(0, text.length - 1);
  77. return text;
  78. }
  79. // Convert markdown within an element, retaining only span-level tags
  80. function convertSpans(text, extra) {
  81. return sanitizeHtml(convertAll(text, extra), inlineTags);
  82. }
  83. // Convert internal markdown using the stock pagedown converter
  84. function convertAll(text, extra) {
  85. var result = extra.blockGamutHookCallback(text);
  86. // We need to perform these operations since we skip the steps in the converter
  87. result = unescapeSpecialChars(result);
  88. result = result.replace(/~D/g, "$$").replace(/~T/g, "~");
  89. result = extra.previousPostConversion(result);
  90. return result;
  91. }
  92. // Convert escaped special characters
  93. function processEscapesStep1(text) {
  94. // Markdown extra adds two escapable characters, `:` and `|`
  95. return text.replace(/\\\|/g, '~I').replace(/\\:/g, '~i');
  96. }
  97. function processEscapesStep2(text) {
  98. return text.replace(/~I/g, '|').replace(/~i/g, ':');
  99. }
  100. // Duplicated from PageDown converter
  101. function unescapeSpecialChars(text) {
  102. // Swap back in all the special characters we've hidden.
  103. text = text.replace(/~E(\d+)E/g, function(wholeMatch, m1) {
  104. var charCodeToReplace = parseInt(m1);
  105. return String.fromCharCode(charCodeToReplace);
  106. });
  107. return text;
  108. }
  109. function slugify(text) {
  110. return text.toLowerCase()
  111. .replace(/\s+/g, '-') // Replace spaces with -
  112. .replace(/[^\w\-]+/g, '') // Remove all non-word chars
  113. .replace(/\-\-+/g, '-') // Replace multiple - with single -
  114. .replace(/^-+/, '') // Trim - from start of text
  115. .replace(/-+$/, ''); // Trim - from end of text
  116. }
  117. /*****************************************************************************
  118. * Markdown.Extra *
  119. ****************************************************************************/
  120. Markdown.Extra = function() {
  121. // For converting internal markdown (in tables for instance).
  122. // This is necessary since these methods are meant to be called as
  123. // preConversion hooks, and the Markdown converter passed to init()
  124. // won't convert any markdown contained in the html tags we return.
  125. this.converter = null;
  126. // Stores html blocks we generate in hooks so that
  127. // they're not destroyed if the user is using a sanitizing converter
  128. this.hashBlocks = [];
  129. // Stores footnotes
  130. this.footnotes = {};
  131. this.usedFootnotes = [];
  132. // Special attribute blocks for fenced code blocks and headers enabled.
  133. this.attributeBlocks = false;
  134. // Fenced code block options
  135. this.googleCodePrettify = false;
  136. this.highlightJs = false;
  137. // Table options
  138. this.tableClass = '';
  139. this.tabWidth = 4;
  140. };
  141. Markdown.Extra.init = function(converter, options) {
  142. // Each call to init creates a new instance of Markdown.Extra so it's
  143. // safe to have multiple converters, with different options, on a single page
  144. var extra = new Markdown.Extra();
  145. var postNormalizationTransformations = [];
  146. var preBlockGamutTransformations = [];
  147. var postSpanGamutTransformations = [];
  148. var postConversionTransformations = ["unHashExtraBlocks"];
  149. options = options || {};
  150. options.extensions = options.extensions || ["all"];
  151. if (contains(options.extensions, "all")) {
  152. options.extensions = ["tables", "fenced_code_gfm", "def_list", "attr_list", "footnotes", "smartypants", "strikethrough", "newlines"];
  153. }
  154. preBlockGamutTransformations.push("wrapHeaders");
  155. if (contains(options.extensions, "attr_list")) {
  156. postNormalizationTransformations.push("hashFcbAttributeBlocks");
  157. preBlockGamutTransformations.push("hashHeaderAttributeBlocks");
  158. postConversionTransformations.push("applyAttributeBlocks");
  159. extra.attributeBlocks = true;
  160. }
  161. if (contains(options.extensions, "fenced_code_gfm")) {
  162. // This step will convert fcb inside list items and blockquotes
  163. preBlockGamutTransformations.push("fencedCodeBlocks");
  164. // This extra step is to prevent html blocks hashing and link definition/footnotes stripping inside fcb
  165. postNormalizationTransformations.push("fencedCodeBlocks");
  166. }
  167. if (contains(options.extensions, "tables")) {
  168. preBlockGamutTransformations.push("tables");
  169. }
  170. if (contains(options.extensions, "def_list")) {
  171. preBlockGamutTransformations.push("definitionLists");
  172. }
  173. if (contains(options.extensions, "footnotes")) {
  174. postNormalizationTransformations.push("stripFootnoteDefinitions");
  175. preBlockGamutTransformations.push("doFootnotes");
  176. postConversionTransformations.push("printFootnotes");
  177. }
  178. if (contains(options.extensions, "smartypants")) {
  179. postConversionTransformations.push("runSmartyPants");
  180. }
  181. if (contains(options.extensions, "strikethrough")) {
  182. postSpanGamutTransformations.push("strikethrough");
  183. }
  184. if (contains(options.extensions, "newlines")) {
  185. postSpanGamutTransformations.push("newlines");
  186. }
  187. converter.hooks.chain("postNormalization", function(text) {
  188. return extra.doTransform(postNormalizationTransformations, text) + '\n';
  189. });
  190. converter.hooks.chain("preBlockGamut", function(text, blockGamutHookCallback) {
  191. // Keep a reference to the block gamut callback to run recursively
  192. extra.blockGamutHookCallback = blockGamutHookCallback;
  193. text = processEscapesStep1(text);
  194. text = extra.doTransform(preBlockGamutTransformations, text) + '\n';
  195. text = processEscapesStep2(text);
  196. return text;
  197. });
  198. converter.hooks.chain("postSpanGamut", function(text) {
  199. return extra.doTransform(postSpanGamutTransformations, text);
  200. });
  201. // Keep a reference to the hook chain running before doPostConversion to apply on hashed extra blocks
  202. extra.previousPostConversion = converter.hooks.postConversion;
  203. converter.hooks.chain("postConversion", function(text) {
  204. text = extra.doTransform(postConversionTransformations, text);
  205. // Clear state vars that may use unnecessary memory
  206. extra.hashBlocks = [];
  207. extra.footnotes = {};
  208. extra.usedFootnotes = [];
  209. return text;
  210. });
  211. if ("highlighter" in options) {
  212. extra.googleCodePrettify = options.highlighter === 'prettify';
  213. extra.highlightJs = options.highlighter === 'highlight';
  214. }
  215. if ("table_class" in options) {
  216. extra.tableClass = options.table_class;
  217. }
  218. extra.converter = converter;
  219. // Caller usually won't need this, but it's handy for testing.
  220. return extra;
  221. };
  222. // Do transformations
  223. Markdown.Extra.prototype.doTransform = function(transformations, text) {
  224. for(var i = 0; i < transformations.length; i++)
  225. text = this[transformations[i]](text);
  226. return text;
  227. };
  228. // Return a placeholder containing a key, which is the block's index in the
  229. // hashBlocks array. We wrap our output in a <p> tag here so Pagedown won't.
  230. Markdown.Extra.prototype.hashExtraBlock = function(block) {
  231. return '\n<p>~X' + (this.hashBlocks.push(block) - 1) + 'X</p>\n';
  232. };
  233. Markdown.Extra.prototype.hashExtraInline = function(block) {
  234. return '~X' + (this.hashBlocks.push(block) - 1) + 'X';
  235. };
  236. // Replace placeholder blocks in `text` with their corresponding
  237. // html blocks in the hashBlocks array.
  238. Markdown.Extra.prototype.unHashExtraBlocks = function(text) {
  239. var self = this;
  240. function recursiveUnHash() {
  241. var hasHash = false;
  242. text = text.replace(/(?:<p>)?~X(\d+)X(?:<\/p>)?/g, function(wholeMatch, m1) {
  243. hasHash = true;
  244. var key = parseInt(m1, 10);
  245. return self.hashBlocks[key];
  246. });
  247. if(hasHash === true) {
  248. recursiveUnHash();
  249. }
  250. }
  251. recursiveUnHash();
  252. return text;
  253. };
  254. // Wrap headers to make sure they won't be in def lists
  255. Markdown.Extra.prototype.wrapHeaders = function(text) {
  256. function wrap(text) {
  257. return '\n' + text + '\n';
  258. }
  259. text = text.replace(/^.+[ \t]*\n=+[ \t]*\n+/gm, wrap);
  260. text = text.replace(/^.+[ \t]*\n-+[ \t]*\n+/gm, wrap);
  261. text = text.replace(/^\#{1,6}[ \t]*.+?[ \t]*\#*\n+/gm, wrap);
  262. return text;
  263. };
  264. /******************************************************************
  265. * Attribute Blocks *
  266. *****************************************************************/
  267. // TODO: use sentinels. Should we just add/remove them in doConversion?
  268. // TODO: better matches for id / class attributes
  269. var attrBlock = "\\{[ \\t]*((?:[#.][-_:a-zA-Z0-9]+[ \\t]*)+)\\}";
  270. var hdrAttributesA = new RegExp("^(#{1,6}.*#{0,6})[ \\t]+" + attrBlock + "[ \\t]*(?:\\n|0x03)", "gm");
  271. var hdrAttributesB = new RegExp("^(.*)[ \\t]+" + attrBlock + "[ \\t]*\\n" +
  272. "(?=[\\-|=]+\\s*(?:\\n|0x03))", "gm"); // underline lookahead
  273. var fcbAttributes = new RegExp("^(```[^`\\n]*)[ \\t]+" + attrBlock + "[ \\t]*\\n" +
  274. "(?=([\\s\\S]*?)\\n```[ \\t]*(\\n|0x03))", "gm");
  275. // Extract headers attribute blocks, move them above the element they will be
  276. // applied to, and hash them for later.
  277. Markdown.Extra.prototype.hashHeaderAttributeBlocks = function(text) {
  278. var self = this;
  279. function attributeCallback(wholeMatch, pre, attr) {
  280. return '<p>~XX' + (self.hashBlocks.push(attr) - 1) + 'XX</p>\n' + pre + "\n";
  281. }
  282. text = text.replace(hdrAttributesA, attributeCallback); // ## headers
  283. text = text.replace(hdrAttributesB, attributeCallback); // underline headers
  284. return text;
  285. };
  286. // Extract FCB attribute blocks, move them above the element they will be
  287. // applied to, and hash them for later.
  288. Markdown.Extra.prototype.hashFcbAttributeBlocks = function(text) {
  289. // TODO: use sentinels. Should we just add/remove them in doConversion?
  290. // TODO: better matches for id / class attributes
  291. var self = this;
  292. function attributeCallback(wholeMatch, pre, attr) {
  293. return '<p>~XX' + (self.hashBlocks.push(attr) - 1) + 'XX</p>\n' + pre + "\n";
  294. }
  295. return text.replace(fcbAttributes, attributeCallback);
  296. };
  297. Markdown.Extra.prototype.applyAttributeBlocks = function(text) {
  298. var self = this;
  299. var blockRe = new RegExp('<p>~XX(\\d+)XX</p>[\\s]*' +
  300. '(?:<(h[1-6]|pre)(?: +class="(\\S+)")?(>[\\s\\S]*?</\\2>))', "gm");
  301. text = text.replace(blockRe, function(wholeMatch, k, tag, cls, rest) {
  302. if (!tag) // no following header or fenced code block.
  303. return '';
  304. // get attributes list from hash
  305. var key = parseInt(k, 10);
  306. var attributes = self.hashBlocks[key];
  307. // get id
  308. var id = attributes.match(/#[^\s#.]+/g) || [];
  309. var idStr = id[0] ? ' id="' + id[0].substr(1, id[0].length - 1) + '"' : '';
  310. // get classes and merge with existing classes
  311. var classes = attributes.match(/\.[^\s#.]+/g) || [];
  312. for (var i = 0; i < classes.length; i++) // Remove leading dot
  313. classes[i] = classes[i].substr(1, classes[i].length - 1);
  314. var classStr = '';
  315. if (cls)
  316. classes = union(classes, [cls]);
  317. if (classes.length > 0)
  318. classStr = ' class="' + classes.join(' ') + '"';
  319. return "<" + tag + idStr + classStr + rest;
  320. });
  321. return text;
  322. };
  323. /******************************************************************
  324. * Tables *
  325. *****************************************************************/
  326. // Find and convert Markdown Extra tables into html.
  327. Markdown.Extra.prototype.tables = function(text) {
  328. var self = this;
  329. var leadingPipe = new RegExp(
  330. ['^' ,
  331. '[ ]{0,3}' , // Allowed whitespace
  332. '[|]' , // Initial pipe
  333. '(.+)\\n' , // $1: Header Row
  334. '[ ]{0,3}' , // Allowed whitespace
  335. '[|]([ ]*[-:]+[-| :]*)\\n' , // $2: Separator
  336. '(' , // $3: Table Body
  337. '(?:[ ]*[|].*\\n?)*' , // Table rows
  338. ')',
  339. '(?:\\n|$)' // Stop at final newline
  340. ].join(''),
  341. 'gm'
  342. );
  343. var noLeadingPipe = new RegExp(
  344. ['^' ,
  345. '[ ]{0,3}' , // Allowed whitespace
  346. '(\\S.*[|].*)\\n' , // $1: Header Row
  347. '[ ]{0,3}' , // Allowed whitespace
  348. '([-:]+[ ]*[|][-| :]*)\\n' , // $2: Separator
  349. '(' , // $3: Table Body
  350. '(?:.*[|].*\\n?)*' , // Table rows
  351. ')' ,
  352. '(?:\\n|$)' // Stop at final newline
  353. ].join(''),
  354. 'gm'
  355. );
  356. text = text.replace(leadingPipe, doTable);
  357. text = text.replace(noLeadingPipe, doTable);
  358. // $1 = header, $2 = separator, $3 = body
  359. function doTable(match, header, separator, body, offset, string) {
  360. // remove any leading pipes and whitespace
  361. header = header.replace(/^ *[|]/m, '');
  362. separator = separator.replace(/^ *[|]/m, '');
  363. body = body.replace(/^ *[|]/gm, '');
  364. // remove trailing pipes and whitespace
  365. header = header.replace(/[|] *$/m, '');
  366. separator = separator.replace(/[|] *$/m, '');
  367. body = body.replace(/[|] *$/gm, '');
  368. // determine column alignments
  369. var alignspecs = separator.split(/ *[|] */);
  370. var align = [];
  371. for (var i = 0; i < alignspecs.length; i++) {
  372. var spec = alignspecs[i];
  373. if (spec.match(/^ *-+: *$/m))
  374. align[i] = ' align="right"';
  375. else if (spec.match(/^ *:-+: *$/m))
  376. align[i] = ' align="center"';
  377. else if (spec.match(/^ *:-+ *$/m))
  378. align[i] = ' align="left"';
  379. else align[i] = '';
  380. }
  381. // TODO: parse spans in header and rows before splitting, so that pipes
  382. // inside of tags are not interpreted as separators
  383. var headers = header.split(/ *[|] */);
  384. var colCount = headers.length;
  385. // build html
  386. var cls = self.tableClass ? ' class="' + self.tableClass + '"' : '';
  387. var html = ['<table', cls, '>\n', '<thead>\n', '<tr>\n'].join('');
  388. // build column headers.
  389. for (i = 0; i < colCount; i++) {
  390. var headerHtml = convertSpans(trim(headers[i]), self);
  391. html += [" <th", align[i], ">", headerHtml, "</th>\n"].join('');
  392. }
  393. html += "</tr>\n</thead>\n";
  394. // build rows
  395. var rows = body.split('\n');
  396. for (i = 0; i < rows.length; i++) {
  397. if (rows[i].match(/^\s*$/)) // can apply to final row
  398. continue;
  399. // ensure number of rowCells matches colCount
  400. var rowCells = rows[i].split(/ *[|] */);
  401. var lenDiff = colCount - rowCells.length;
  402. for (var j = 0; j < lenDiff; j++)
  403. rowCells.push('');
  404. html += "<tr>\n";
  405. for (j = 0; j < colCount; j++) {
  406. var colHtml = convertSpans(trim(rowCells[j]), self);
  407. html += [" <td", align[j], ">", colHtml, "</td>\n"].join('');
  408. }
  409. html += "</tr>\n";
  410. }
  411. html += "</table>\n";
  412. // replace html with placeholder until postConversion step
  413. return self.hashExtraBlock(html);
  414. }
  415. return text;
  416. };
  417. /******************************************************************
  418. * Footnotes *
  419. *****************************************************************/
  420. // Strip footnote, store in hashes.
  421. Markdown.Extra.prototype.stripFootnoteDefinitions = function(text) {
  422. var self = this;
  423. text = text.replace(
  424. /\n[ ]{0,3}\[\^(.+?)\]\:[ \t]*\n?([\s\S]*?)\n{1,2}((?=\n[ ]{0,3}\S)|$)/g,
  425. function(wholeMatch, m1, m2) {
  426. m1 = slugify(m1);
  427. m2 += "\n";
  428. m2 = m2.replace(/^[ ]{0,3}/g, "");
  429. self.footnotes[m1] = m2;
  430. return "\n";
  431. });
  432. return text;
  433. };
  434. // Find and convert footnotes references.
  435. Markdown.Extra.prototype.doFootnotes = function(text) {
  436. var self = this;
  437. if(self.isConvertingFootnote === true) {
  438. return text;
  439. }
  440. var footnoteCounter = 0;
  441. text = text.replace(/\[\^(.+?)\]/g, function(wholeMatch, m1) {
  442. var id = slugify(m1);
  443. var footnote = self.footnotes[id];
  444. if (footnote === undefined) {
  445. return wholeMatch;
  446. }
  447. footnoteCounter++;
  448. self.usedFootnotes.push(id);
  449. var html = '<a href="#fn:' + id + '" id="fnref:' + id
  450. + '" title="See footnote" class="footnote">' + footnoteCounter
  451. + '</a>';
  452. return self.hashExtraInline(html);
  453. });
  454. return text;
  455. };
  456. // Print footnotes at the end of the document
  457. Markdown.Extra.prototype.printFootnotes = function(text) {
  458. var self = this;
  459. if (self.usedFootnotes.length === 0) {
  460. return text;
  461. }
  462. text += '\n\n<div class="footnotes">\n<hr>\n<ol>\n\n';
  463. for(var i=0; i<self.usedFootnotes.length; i++) {
  464. var id = self.usedFootnotes[i];
  465. var footnote = self.footnotes[id];
  466. self.isConvertingFootnote = true;
  467. var formattedfootnote = convertSpans(footnote, self);
  468. delete self.isConvertingFootnote;
  469. text += '<li id="fn:'
  470. + id
  471. + '">'
  472. + formattedfootnote
  473. + ' <a href="#fnref:'
  474. + id
  475. + '" title="Return to article" class="reversefootnote">&#8617;</a></li>\n\n';
  476. }
  477. text += '</ol>\n</div>';
  478. return text;
  479. };
  480. /******************************************************************
  481. * Fenced Code Blocks (gfm) *
  482. ******************************************************************/
  483. // Find and convert gfm-inspired fenced code blocks into html.
  484. Markdown.Extra.prototype.fencedCodeBlocks = function(text) {
  485. function encodeCode(code) {
  486. code = code.replace(/&/g, "&amp;");
  487. code = code.replace(/</g, "&lt;");
  488. code = code.replace(/>/g, "&gt;");
  489. // These were escaped by PageDown before postNormalization
  490. code = code.replace(/~D/g, "$$");
  491. code = code.replace(/~T/g, "~");
  492. return code;
  493. }
  494. var self = this;
  495. text = text.replace(/(?:^|\n)```([^`\n]*)\n([\s\S]*?)\n```[ \t]*(?=\n)/g, function(match, m1, m2) {
  496. var language = trim(m1), codeblock = m2;
  497. // adhere to specified options
  498. var preclass = self.googleCodePrettify ? ' class="prettyprint"' : '';
  499. var codeclass = '';
  500. if (language) {
  501. if (self.googleCodePrettify || self.highlightJs) {
  502. // use html5 language- class names. supported by both prettify and highlight.js
  503. codeclass = ' class="language-' + language + '"';
  504. } else {
  505. codeclass = ' class="' + language + '"';
  506. }
  507. }
  508. var html = ['<pre', preclass, '><code', codeclass, '>',
  509. encodeCode(codeblock), '</code></pre>'].join('');
  510. // replace codeblock with placeholder until postConversion step
  511. return self.hashExtraBlock(html);
  512. });
  513. return text;
  514. };
  515. /******************************************************************
  516. * SmartyPants *
  517. ******************************************************************/
  518. Markdown.Extra.prototype.educatePants = function(text) {
  519. var self = this;
  520. var result = '';
  521. var blockOffset = 0;
  522. // Here we parse HTML in a very bad manner
  523. text.replace(/(?:<!--[\s\S]*?-->)|(<)([a-zA-Z1-6]+)([^\n]*?>)([\s\S]*?)(<\/\2>)/g, function(wholeMatch, m1, m2, m3, m4, m5, offset) {
  524. var token = text.substring(blockOffset, offset);
  525. result += self.applyPants(token);
  526. self.smartyPantsLastChar = result.substring(result.length - 1);
  527. blockOffset = offset + wholeMatch.length;
  528. if(!m1) {
  529. // Skip commentary
  530. result += wholeMatch;
  531. return;
  532. }
  533. // Skip special tags
  534. if(!/code|kbd|pre|script|noscript|iframe|math|ins|del|pre/i.test(m2)) {
  535. m4 = self.educatePants(m4);
  536. }
  537. else {
  538. self.smartyPantsLastChar = m4.substring(m4.length - 1);
  539. }
  540. result += m1 + m2 + m3 + m4 + m5;
  541. });
  542. var lastToken = text.substring(blockOffset);
  543. result += self.applyPants(lastToken);
  544. self.smartyPantsLastChar = result.substring(result.length - 1);
  545. return result;
  546. };
  547. function revertPants(wholeMatch, m1) {
  548. var blockText = m1;
  549. blockText = blockText.replace(/&\#8220;/g, "\"");
  550. blockText = blockText.replace(/&\#8221;/g, "\"");
  551. blockText = blockText.replace(/&\#8216;/g, "'");
  552. blockText = blockText.replace(/&\#8217;/g, "'");
  553. blockText = blockText.replace(/&\#8212;/g, "---");
  554. blockText = blockText.replace(/&\#8211;/g, "--");
  555. blockText = blockText.replace(/&\#8230;/g, "...");
  556. return blockText;
  557. }
  558. Markdown.Extra.prototype.applyPants = function(text) {
  559. // Dashes
  560. text = text.replace(/---/g, "&#8212;").replace(/--/g, "&#8211;");
  561. // Ellipses
  562. text = text.replace(/\.\.\./g, "&#8230;").replace(/\.\s\.\s\./g, "&#8230;");
  563. // Backticks
  564. text = text.replace(/``/g, "&#8220;").replace (/''/g, "&#8221;");
  565. if(/^'$/.test(text)) {
  566. // Special case: single-character ' token
  567. if(/\S/.test(this.smartyPantsLastChar)) {
  568. return "&#8217;";
  569. }
  570. return "&#8216;";
  571. }
  572. if(/^"$/.test(text)) {
  573. // Special case: single-character " token
  574. if(/\S/.test(this.smartyPantsLastChar)) {
  575. return "&#8221;";
  576. }
  577. return "&#8220;";
  578. }
  579. // Special case if the very first character is a quote
  580. // followed by punctuation at a non-word-break. Close the quotes by brute force:
  581. text = text.replace (/^'(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "&#8217;");
  582. text = text.replace (/^"(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "&#8221;");
  583. // Special case for double sets of quotes, e.g.:
  584. // <p>He said, "'Quoted' words in a larger quote."</p>
  585. text = text.replace(/"'(?=\w)/g, "&#8220;&#8216;");
  586. text = text.replace(/'"(?=\w)/g, "&#8216;&#8220;");
  587. // Special case for decade abbreviations (the '80s):
  588. text = text.replace(/'(?=\d{2}s)/g, "&#8217;");
  589. // Get most opening single quotes:
  590. text = text.replace(/(\s|&nbsp;|--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)'(?=\w)/g, "$1&#8216;");
  591. // Single closing quotes:
  592. text = text.replace(/([^\s\[\{\(\-])'/g, "$1&#8217;");
  593. text = text.replace(/'(?=\s|s\b)/g, "&#8217;");
  594. // Any remaining single quotes should be opening ones:
  595. text = text.replace(/'/g, "&#8216;");
  596. // Get most opening double quotes:
  597. text = text.replace(/(\s|&nbsp;|--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)"(?=\w)/g, "$1&#8220;");
  598. // Double closing quotes:
  599. text = text.replace(/([^\s\[\{\(\-])"/g, "$1&#8221;");
  600. text = text.replace(/"(?=\s)/g, "&#8221;");
  601. // Any remaining quotes should be opening ones.
  602. text = text.replace(/"/ig, "&#8220;");
  603. return text;
  604. };
  605. // Find and convert markdown extra definition lists into html.
  606. Markdown.Extra.prototype.runSmartyPants = function(text) {
  607. this.smartyPantsLastChar = '';
  608. text = this.educatePants(text);
  609. // Clean everything inside html tags (some of them may have been converted due to our rough html parsing)
  610. text = text.replace(/(<([a-zA-Z1-6]+)\b([^\n>]*?)(\/)?>)/g, revertPants);
  611. return text;
  612. };
  613. /******************************************************************
  614. * Definition Lists *
  615. ******************************************************************/
  616. // Find and convert markdown extra definition lists into html.
  617. Markdown.Extra.prototype.definitionLists = function(text) {
  618. var wholeList = new RegExp(
  619. ['(\\x02\\n?|\\n\\n)' ,
  620. '(?:' ,
  621. '(' , // $1 = whole list
  622. '(' , // $2
  623. '[ ]{0,3}' ,
  624. '((?:[ \\t]*\\S.*\\n)+)', // $3 = defined term
  625. '\\n?' ,
  626. '[ ]{0,3}:[ ]+' , // colon starting definition
  627. ')' ,
  628. '([\\s\\S]+?)' ,
  629. '(' , // $4
  630. '(?=\\0x03)' , // \z
  631. '|' ,
  632. '(?=' ,
  633. '\\n{2,}' ,
  634. '(?=\\S)' ,
  635. '(?!' , // Negative lookahead for another term
  636. '[ ]{0,3}' ,
  637. '(?:\\S.*\\n)+?' , // defined term
  638. '\\n?' ,
  639. '[ ]{0,3}:[ ]+' , // colon starting definition
  640. ')' ,
  641. '(?!' , // Negative lookahead for another definition
  642. '[ ]{0,3}:[ ]+' , // colon starting definition
  643. ')' ,
  644. ')' ,
  645. ')' ,
  646. ')' ,
  647. ')'
  648. ].join(''),
  649. 'gm'
  650. );
  651. var self = this;
  652. text = addAnchors(text);
  653. text = text.replace(wholeList, function(match, pre, list) {
  654. var result = trim(self.processDefListItems(list));
  655. result = "<dl>\n" + result + "\n</dl>";
  656. return pre + self.hashExtraBlock(result) + "\n\n";
  657. });
  658. return removeAnchors(text);
  659. };
  660. // Process the contents of a single definition list, splitting it
  661. // into individual term and definition list items.
  662. Markdown.Extra.prototype.processDefListItems = function(listStr) {
  663. var self = this;
  664. var dt = new RegExp(
  665. ['(\\x02\\n?|\\n\\n+)' , // leading line
  666. '(' , // definition terms = $1
  667. '[ ]{0,3}' , // leading whitespace
  668. '(?![:][ ]|[ ])' , // negative lookahead for a definition
  669. // mark (colon) or more whitespace
  670. '(?:\\S.*\\n)+?' , // actual term (not whitespace)
  671. ')' ,
  672. '(?=\\n?[ ]{0,3}:[ ])' // lookahead for following line feed
  673. ].join(''), // with a definition mark
  674. 'gm'
  675. );
  676. var dd = new RegExp(
  677. ['\\n(\\n+)?' , // leading line = $1
  678. '(' , // marker space = $2
  679. '[ ]{0,3}' , // whitespace before colon
  680. '[:][ ]+' , // definition mark (colon)
  681. ')' ,
  682. '([\\s\\S]+?)' , // definition text = $3
  683. '(?=\\n*' , // stop at next definition mark,
  684. '(?:' , // next term or end of text
  685. '\\n[ ]{0,3}[:][ ]|' ,
  686. '<dt>|\\x03' , // \z
  687. ')' ,
  688. ')'
  689. ].join(''),
  690. 'gm'
  691. );
  692. listStr = addAnchors(listStr);
  693. // trim trailing blank lines:
  694. listStr = listStr.replace(/\n{2,}(?=\\x03)/, "\n");
  695. // Process definition terms.
  696. listStr = listStr.replace(dt, function(match, pre, termsStr) {
  697. var terms = trim(termsStr).split("\n");
  698. var text = '';
  699. for (var i = 0; i < terms.length; i++) {
  700. var term = terms[i];
  701. // process spans inside dt
  702. term = convertSpans(trim(term), self);
  703. text += "\n<dt>" + term + "</dt>";
  704. }
  705. return text + "\n";
  706. });
  707. // Process actual definitions.
  708. listStr = listStr.replace(dd, function(match, leadingLine, markerSpace, def) {
  709. if (leadingLine || def.match(/\n{2,}/)) {
  710. // replace marker with the appropriate whitespace indentation
  711. def = Array(markerSpace.length + 1).join(' ') + def;
  712. // process markdown inside definition
  713. // TODO?: currently doesn't apply extensions
  714. def = outdent(def) + "\n\n";
  715. def = "\n" + convertAll(def, self) + "\n";
  716. } else {
  717. // convert span-level markdown inside definition
  718. def = rtrim(def);
  719. def = convertSpans(outdent(def), self);
  720. }
  721. return "\n<dd>" + def + "</dd>\n";
  722. });
  723. return removeAnchors(listStr);
  724. };
  725. /***********************************************************
  726. * Strikethrough *
  727. ************************************************************/
  728. Markdown.Extra.prototype.strikethrough = function(text) {
  729. // Pretty much duplicated from _DoItalicsAndBold
  730. return text.replace(/([\W_]|^)~T~T(?=\S)([^\r]*?\S[\*_]*)~T~T([\W_]|$)/g,
  731. "$1<del>$2</del>$3");
  732. };
  733. /***********************************************************
  734. * New lines *
  735. ************************************************************/
  736. Markdown.Extra.prototype.newlines = function(text) {
  737. // We have to ignore already converted newlines and line breaks in sub-list items
  738. return text.replace(/(<(?:br|\/li)>)?\n/g, function(wholeMatch, previousTag) {
  739. return previousTag ? wholeMatch : " <br>\n";
  740. });
  741. };
  742. })();