converter.js 27 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040
  1. /**
  2. * Created by Estevao on 31-05-2015.
  3. */
  4. /**
  5. * Showdown Converter class
  6. * @class
  7. * @param {object} [converterOptions]
  8. * @returns {Converter}
  9. */
  10. showdown.Converter = function (converterOptions) {
  11. 'use strict';
  12. var
  13. /**
  14. * Options used by this converter
  15. * @private
  16. * @type {{}}
  17. */
  18. options = {},
  19. /**
  20. * Language extensions used by this converter
  21. * @private
  22. * @type {Array}
  23. */
  24. langExtensions = [],
  25. /**
  26. * Output modifiers extensions used by this converter
  27. * @private
  28. * @type {Array}
  29. */
  30. outputModifiers = [],
  31. /**
  32. * Event listeners
  33. * @private
  34. * @type {{}}
  35. */
  36. listeners = {},
  37. /**
  38. * The flavor set in this converter
  39. */
  40. setConvFlavor = setFlavor,
  41. /**
  42. * Metadata of the document
  43. * @type {{parsed: {}, raw: string, format: string}}
  44. */
  45. metadata = {
  46. parsed: {},
  47. raw: '',
  48. format: ''
  49. };
  50. _constructor();
  51. /**
  52. * Converter constructor
  53. * @private
  54. */
  55. function _constructor () {
  56. converterOptions = converterOptions || {};
  57. for (var gOpt in globalOptions) {
  58. if (globalOptions.hasOwnProperty(gOpt)) {
  59. options[gOpt] = globalOptions[gOpt];
  60. }
  61. }
  62. // Merge options
  63. if (typeof converterOptions === 'object') {
  64. for (var opt in converterOptions) {
  65. if (converterOptions.hasOwnProperty(opt)) {
  66. options[opt] = converterOptions[opt];
  67. }
  68. }
  69. } else {
  70. throw Error('Converter expects the passed parameter to be an object, but ' + typeof converterOptions +
  71. ' was passed instead.');
  72. }
  73. if (options.extensions) {
  74. showdown.helper.forEach(options.extensions, _parseExtension);
  75. }
  76. }
  77. /**
  78. * Parse extension
  79. * @param {*} ext
  80. * @param {string} [name='']
  81. * @private
  82. */
  83. function _parseExtension (ext, name) {
  84. name = name || null;
  85. // If it's a string, the extension was previously loaded
  86. if (showdown.helper.isString(ext)) {
  87. ext = showdown.helper.stdExtName(ext);
  88. name = ext;
  89. // LEGACY_SUPPORT CODE
  90. if (showdown.extensions[ext]) {
  91. console.warn('DEPRECATION WARNING: ' + ext + ' is an old extension that uses a deprecated loading method.' +
  92. 'Please inform the developer that the extension should be updated!');
  93. legacyExtensionLoading(showdown.extensions[ext], ext);
  94. return;
  95. // END LEGACY SUPPORT CODE
  96. } else if (!showdown.helper.isUndefined(extensions[ext])) {
  97. ext = extensions[ext];
  98. } else {
  99. throw Error('Extension "' + ext + '" could not be loaded. It was either not found or is not a valid extension.');
  100. }
  101. }
  102. if (typeof ext === 'function') {
  103. ext = ext();
  104. }
  105. if (!showdown.helper.isArray(ext)) {
  106. ext = [ext];
  107. }
  108. var validExt = validate(ext, name);
  109. if (!validExt.valid) {
  110. throw Error(validExt.error);
  111. }
  112. for (var i = 0; i < ext.length; ++i) {
  113. switch (ext[i].type) {
  114. case 'lang':
  115. langExtensions.push(ext[i]);
  116. break;
  117. case 'output':
  118. outputModifiers.push(ext[i]);
  119. break;
  120. }
  121. if (ext[i].hasOwnProperty('listeners')) {
  122. for (var ln in ext[i].listeners) {
  123. if (ext[i].listeners.hasOwnProperty(ln)) {
  124. listen(ln, ext[i].listeners[ln]);
  125. }
  126. }
  127. }
  128. }
  129. }
  130. /**
  131. * LEGACY_SUPPORT
  132. * @param {*} ext
  133. * @param {string} name
  134. */
  135. function legacyExtensionLoading (ext, name) {
  136. if (typeof ext === 'function') {
  137. ext = ext(new showdown.Converter());
  138. }
  139. if (!showdown.helper.isArray(ext)) {
  140. ext = [ext];
  141. }
  142. var valid = validate(ext, name);
  143. if (!valid.valid) {
  144. throw Error(valid.error);
  145. }
  146. for (var i = 0; i < ext.length; ++i) {
  147. switch (ext[i].type) {
  148. case 'lang':
  149. langExtensions.push(ext[i]);
  150. break;
  151. case 'output':
  152. outputModifiers.push(ext[i]);
  153. break;
  154. default:// should never reach here
  155. throw Error('Extension loader error: Type unrecognized!!!');
  156. }
  157. }
  158. }
  159. /**
  160. * Listen to an event
  161. * @param {string} name
  162. * @param {function} callback
  163. */
  164. function listen (name, callback) {
  165. if (!showdown.helper.isString(name)) {
  166. throw Error('Invalid argument in converter.listen() method: name must be a string, but ' + typeof name + ' given');
  167. }
  168. if (typeof callback !== 'function') {
  169. throw Error('Invalid argument in converter.listen() method: callback must be a function, but ' + typeof callback + ' given');
  170. }
  171. if (!listeners.hasOwnProperty(name)) {
  172. listeners[name] = [];
  173. }
  174. listeners[name].push(callback);
  175. }
  176. function rTrimInputText (text) {
  177. var rsp = text.match(/^\s*/)[0].length,
  178. rgx = new RegExp('^\\s{0,' + rsp + '}', 'gm');
  179. return text.replace(rgx, '');
  180. }
  181. /**
  182. * Dispatch an event
  183. * @private
  184. * @param {string} evtName Event name
  185. * @param {string} text Text
  186. * @param {{}} options Converter Options
  187. * @param {{}} globals
  188. * @returns {string}
  189. */
  190. this._dispatch = function dispatch (evtName, text, options, globals) {
  191. if (listeners.hasOwnProperty(evtName)) {
  192. for (var ei = 0; ei < listeners[evtName].length; ++ei) {
  193. var nText = listeners[evtName][ei](evtName, text, this, options, globals);
  194. if (nText && typeof nText !== 'undefined') {
  195. text = nText;
  196. }
  197. }
  198. }
  199. return text;
  200. };
  201. /**
  202. * Listen to an event
  203. * @param {string} name
  204. * @param {function} callback
  205. * @returns {showdown.Converter}
  206. */
  207. this.listen = function (name, callback) {
  208. listen(name, callback);
  209. return this;
  210. };
  211. /**
  212. * Converts a markdown string into HTML string
  213. * @param {string} text
  214. * @returns {*}
  215. */
  216. this.makeHtml = function (text) {
  217. //check if text is not falsy
  218. if (!text) {
  219. return text;
  220. }
  221. var globals = {
  222. gHtmlBlocks: [],
  223. gHtmlMdBlocks: [],
  224. gHtmlSpans: [],
  225. gUrls: {},
  226. gTitles: {},
  227. gDimensions: {},
  228. gListLevel: 0,
  229. hashLinkCounts: {},
  230. langExtensions: langExtensions,
  231. outputModifiers: outputModifiers,
  232. converter: this,
  233. ghCodeBlocks: [],
  234. metadata: {
  235. parsed: {},
  236. raw: '',
  237. format: ''
  238. }
  239. };
  240. // This lets us use ¨ trema as an escape char to avoid md5 hashes
  241. // The choice of character is arbitrary; anything that isn't
  242. // magic in Markdown will work.
  243. text = text.replace(/¨/g, '¨T');
  244. // Replace $ with ¨D
  245. // RegExp interprets $ as a special character
  246. // when it's in a replacement string
  247. text = text.replace(/\$/g, '¨D');
  248. // Standardize line endings
  249. text = text.replace(/\r\n/g, '\n'); // DOS to Unix
  250. text = text.replace(/\r/g, '\n'); // Mac to Unix
  251. // Stardardize line spaces (nbsp causes trouble in older browsers and some regex flavors)
  252. text = text.replace(/\u00A0/g, ' ');
  253. if (options.smartIndentationFix) {
  254. text = rTrimInputText(text);
  255. }
  256. // Make sure text begins and ends with a couple of newlines:
  257. text = '\n\n' + text + '\n\n';
  258. // detab
  259. text = showdown.subParser('makehtml.detab')(text, options, globals);
  260. /**
  261. * Strip any lines consisting only of spaces and tabs.
  262. * This makes subsequent regexs easier to write, because we can
  263. * match consecutive blank lines with /\n+/ instead of something
  264. * contorted like /[ \t]*\n+/
  265. */
  266. text = text.replace(/^[ \t]+$/mg, '');
  267. //run languageExtensions
  268. showdown.helper.forEach(langExtensions, function (ext) {
  269. text = showdown.subParser('makehtml.runExtension')(ext, text, options, globals);
  270. });
  271. // run the sub parsers
  272. text = showdown.subParser('makehtml.metadata')(text, options, globals);
  273. text = showdown.subParser('makehtml.hashPreCodeTags')(text, options, globals);
  274. text = showdown.subParser('makehtml.githubCodeBlocks')(text, options, globals);
  275. text = showdown.subParser('makehtml.hashHTMLBlocks')(text, options, globals);
  276. text = showdown.subParser('makehtml.hashCodeTags')(text, options, globals);
  277. text = showdown.subParser('makehtml.stripLinkDefinitions')(text, options, globals);
  278. text = showdown.subParser('makehtml.blockGamut')(text, options, globals);
  279. text = showdown.subParser('makehtml.unhashHTMLSpans')(text, options, globals);
  280. text = showdown.subParser('makehtml.unescapeSpecialChars')(text, options, globals);
  281. // attacklab: Restore dollar signs
  282. text = text.replace(/¨D/g, '$$');
  283. // attacklab: Restore tremas
  284. text = text.replace(/¨T/g, '¨');
  285. // render a complete html document instead of a partial if the option is enabled
  286. text = showdown.subParser('makehtml.completeHTMLDocument')(text, options, globals);
  287. // Run output modifiers
  288. showdown.helper.forEach(outputModifiers, function (ext) {
  289. text = showdown.subParser('makehtml.runExtension')(ext, text, options, globals);
  290. });
  291. // update metadata
  292. metadata = globals.metadata;
  293. return text;
  294. };
  295. /**
  296. * Converts an HTML string into a markdown string
  297. * @param src
  298. * @returns {string}
  299. */
  300. this.makeMarkdown = function (src) {
  301. // replace \r\n with \n
  302. src = src.replace(/\r\n/g, '\n');
  303. src = src.replace(/\r/g, '\n'); // old macs
  304. // due to an edge case, we need to find this: > <
  305. // to prevent removing of non silent white spaces
  306. // ex: <em>this is</em> <strong>sparta</strong>
  307. src = src.replace(/>[ \t]+</, '>¨NBSP;<');
  308. var doc = showdown.helper.document.createElement('div');
  309. doc.innerHTML = src;
  310. var preList = substitutePreCodeTags(doc);
  311. // remove all newlines and collapse spaces
  312. clean(doc);
  313. // some stuff, like accidental reference links must now be escaped
  314. // TODO
  315. // doc.innerHTML = doc.innerHTML.replace(/\[[\S\t ]]/);
  316. var nodes = doc.childNodes,
  317. mdDoc = '';
  318. for (var i = 0; i < nodes.length; i++) {
  319. mdDoc += parseNode(nodes[i]);
  320. }
  321. function parseNode (node, spansOnly) {
  322. spansOnly = spansOnly || false;
  323. var txt = '';
  324. // edge case of text without wrapper paragraph
  325. if (node.nodeType === 3) {
  326. return parseTxt(node);
  327. }
  328. // HTML comment
  329. if (node.nodeType === 8) {
  330. return '<!--' + node.data + '-->\n\n';
  331. }
  332. // process only node elements
  333. if (node.nodeType !== 1) {
  334. return '';
  335. }
  336. var tagName = node.tagName.toLowerCase();
  337. switch (tagName) {
  338. //
  339. // BLOCKS
  340. //
  341. case 'h1':
  342. if (!spansOnly) { txt = parseHeader(node, 1) + '\n\n'; }
  343. break;
  344. case 'h2':
  345. if (!spansOnly) { txt = parseHeader(node, 2) + '\n\n'; }
  346. break;
  347. case 'h3':
  348. if (!spansOnly) { txt = parseHeader(node, 3) + '\n\n'; }
  349. break;
  350. case 'h4':
  351. if (!spansOnly) { txt = parseHeader(node, 4) + '\n\n'; }
  352. break;
  353. case 'h5':
  354. if (!spansOnly) { txt = parseHeader(node, 5) + '\n\n'; }
  355. break;
  356. case 'h6':
  357. if (!spansOnly) { txt = parseHeader(node, 6) + '\n\n'; }
  358. break;
  359. case 'p':
  360. if (!spansOnly) { txt = parseParagraph(node) + '\n\n'; }
  361. break;
  362. case 'blockquote':
  363. if (!spansOnly) { txt = parseBlockquote(node) + '\n\n'; }
  364. break;
  365. case 'hr':
  366. if (!spansOnly) { txt = parseHr(node) + '\n\n'; }
  367. break;
  368. case 'ol':
  369. if (!spansOnly) { txt = parseList(node, 'ol') + '\n\n'; }
  370. break;
  371. case 'ul':
  372. if (!spansOnly) { txt = parseList(node, 'ul') + '\n\n'; }
  373. break;
  374. case 'precode':
  375. if (!spansOnly) { txt = parsePreCode(node) + '\n\n'; }
  376. break;
  377. case 'pre':
  378. if (!spansOnly) { txt = parsePre(node) + '\n\n'; }
  379. break;
  380. case 'table':
  381. if (!spansOnly) { txt = parseTable(node) + '\n\n'; }
  382. break;
  383. //
  384. // SPANS
  385. //
  386. case 'code':
  387. txt = parseCodeSpan(node);
  388. break;
  389. case 'em':
  390. case 'i':
  391. txt = parseEmphasis(node);
  392. break;
  393. case 'strong':
  394. case 'b':
  395. txt = parseStrong(node);
  396. break;
  397. case 'del':
  398. txt = parseDel(node);
  399. break;
  400. case 'a':
  401. txt = parseLinks(node);
  402. break;
  403. case 'img':
  404. txt = parseImage(node);
  405. break;
  406. default:
  407. txt = node.outerHTML + '\n\n';
  408. }
  409. // common normalization
  410. return txt;
  411. }
  412. function parseTxt (node) {
  413. var txt = node.nodeValue;
  414. // multiple spaces are collapsed
  415. txt = txt.replace(/ +/g, ' ');
  416. // replace the custom ¨NBSP; with a space
  417. txt = txt.replace(/¨NBSP;/g, ' ');
  418. // ", <, > and & should replace escaped html entities
  419. txt = showdown.helper.unescapeHTMLEntities(txt);
  420. // escape markdown magic characters
  421. // emphasis, strong and strikethrough - can appear everywhere
  422. // we also escape pipe (|) because of tables
  423. // and escape ` because of code blocks and spans
  424. txt = txt.replace(/([*_~|`])/g, '\\$1');
  425. // escape > because of blockquotes
  426. txt = txt.replace(/^(\s*)>/g, '\\$1>');
  427. // hash character, only troublesome at the beginning of a line because of headers
  428. txt = txt.replace(/^#/gm, '\\#');
  429. // horizontal rules
  430. txt = txt.replace(/^(\s*)([-=]{3,})(\s*)$/, '$1\\$2$3');
  431. // dot, because of ordered lists, only troublesome at the beginning of a line when preceded by an integer
  432. txt = txt.replace(/^( {0,3}\d+)\./gm, '$1\\.');
  433. // +, * and -, at the beginning of a line becomes a list, so we need to escape them also (asterisk was already escaped)
  434. txt = txt.replace(/^( {0,3})([+-])/gm, '$1\\$2');
  435. // images and links, ] followed by ( is problematic, so we escape it
  436. txt = txt.replace(/]([\s]*)\(/g, '\\]$1\\(');
  437. // reference URIs must also be escaped
  438. txt = txt.replace(/^ {0,3}\[([\S \t]*?)]:/gm, '\\[$1]:');
  439. return txt;
  440. }
  441. function parseList (node, type) {
  442. var txt = '';
  443. if (!node.hasChildNodes()) {
  444. return '';
  445. }
  446. var listItems = node.childNodes,
  447. listItemsLenght = listItems.length,
  448. listNum = node.getAttribute('start') || 1;
  449. for (var i = 0; i < listItemsLenght; ++i) {
  450. if (typeof listItems[i].tagName === 'undefined' || listItems[i].tagName.toLowerCase() !== 'li') {
  451. continue;
  452. }
  453. // define the bullet to use in list
  454. var bullet = '';
  455. if (type === 'ol') {
  456. bullet = listNum.toString() + '. ';
  457. } else {
  458. bullet = '- ';
  459. }
  460. // parse list item
  461. txt += bullet + parseListItem(listItems[i]);
  462. ++listNum;
  463. }
  464. return txt.trim();
  465. }
  466. function parseListItem (node) {
  467. var listItemTxt = '';
  468. var children = node.childNodes,
  469. childrenLenght = children.length;
  470. for (var i = 0; i < childrenLenght; ++i) {
  471. listItemTxt += parseNode(children[i]);
  472. }
  473. // if it's only one liner, we need to add a newline at the end
  474. if (!/\n$/.test(listItemTxt)) {
  475. listItemTxt += '\n';
  476. } else {
  477. // it's multiparagraph, so we need to indent
  478. listItemTxt = listItemTxt
  479. .split('\n')
  480. .join('\n ')
  481. .replace(/^ {4}$/gm, '')
  482. .replace(/\n\n+/g, '\n\n');
  483. }
  484. return listItemTxt;
  485. }
  486. function parseHr () {
  487. return '---';
  488. }
  489. function parseBlockquote (node) {
  490. var txt = '';
  491. if (node.hasChildNodes()) {
  492. var children = node.childNodes,
  493. childrenLength = children.length;
  494. for (var i = 0; i < childrenLength; ++i) {
  495. var innerTxt = parseNode(children[i]);
  496. if (innerTxt === '') {
  497. continue;
  498. }
  499. txt += innerTxt;
  500. }
  501. }
  502. // cleanup
  503. txt = txt.trim();
  504. txt = '> ' + txt.split('\n').join('\n> ');
  505. return txt;
  506. }
  507. function parseCodeSpan (node) {
  508. return '`' + node.innerHTML + '`';
  509. }
  510. function parseStrong (node) {
  511. var txt = '';
  512. if (node.hasChildNodes()) {
  513. txt += '**';
  514. var children = node.childNodes,
  515. childrenLength = children.length;
  516. for (var i = 0; i < childrenLength; ++i) {
  517. txt += parseNode(children[i]);
  518. }
  519. txt += '**';
  520. }
  521. return txt;
  522. }
  523. function parseEmphasis (node) {
  524. var txt = '';
  525. if (node.hasChildNodes()) {
  526. txt += '*';
  527. var children = node.childNodes,
  528. childrenLength = children.length;
  529. for (var i = 0; i < childrenLength; ++i) {
  530. txt += parseNode(children[i]);
  531. }
  532. txt += '*';
  533. }
  534. return txt;
  535. }
  536. function parseDel (node) {
  537. var txt = '';
  538. if (node.hasChildNodes()) {
  539. txt += '~~';
  540. var children = node.childNodes,
  541. childrenLength = children.length;
  542. for (var i = 0; i < childrenLength; ++i) {
  543. txt += parseNode(children[i]);
  544. }
  545. txt += '~~';
  546. }
  547. return txt;
  548. }
  549. function parseLinks (node) {
  550. var txt = '';
  551. if (node.hasChildNodes() && node.hasAttribute('href')) {
  552. var children = node.childNodes,
  553. childrenLength = children.length;
  554. txt = '[';
  555. for (var i = 0; i < childrenLength; ++i) {
  556. txt += parseNode(children[i]);
  557. }
  558. txt += '](';
  559. txt += '<' + node.getAttribute('href') + '>';
  560. if (node.hasAttribute('title')) {
  561. txt += ' "' + node.getAttribute('title') + '"';
  562. }
  563. txt += ')';
  564. }
  565. return txt;
  566. }
  567. function parseImage (node) {
  568. var txt = '';
  569. if (node.hasAttribute('src')) {
  570. txt += '![' + node.getAttribute('alt') + '](';
  571. txt += '<' + node.getAttribute('src') + '>';
  572. if (node.hasAttribute('width') && node.hasAttribute('height')) {
  573. txt += ' =' + node.getAttribute('width') + 'x' + node.getAttribute('height');
  574. }
  575. if (node.hasAttribute('title')) {
  576. txt += ' "' + node.getAttribute('title') + '"';
  577. }
  578. txt += ')';
  579. }
  580. return txt;
  581. }
  582. function parseHeader (node, headerLevel) {
  583. var headerMark = new Array(headerLevel + 1).join('#'),
  584. txt = '';
  585. if (node.hasChildNodes()) {
  586. txt = headerMark + ' ';
  587. var children = node.childNodes,
  588. childrenLength = children.length;
  589. for (var i = 0; i < childrenLength; ++i) {
  590. txt += parseNode(children[i]);
  591. }
  592. }
  593. return txt;
  594. }
  595. function parseParagraph (node) {
  596. var txt = '';
  597. if (node.hasChildNodes()) {
  598. var children = node.childNodes,
  599. childrenLength = children.length;
  600. for (var i = 0; i < childrenLength; ++i) {
  601. txt += parseNode(children[i]);
  602. }
  603. }
  604. // some text normalization
  605. txt = txt.trim();
  606. return txt;
  607. }
  608. function parsePreCode (node) {
  609. var lang = node.getAttribute('language'),
  610. num = node.getAttribute('precodenum');
  611. return '```' + lang + '\n' + preList[num] + '\n```';
  612. }
  613. function parsePre (node) {
  614. var num = node.getAttribute('prenum');
  615. return '<pre>' + preList[num] + '</pre>';
  616. }
  617. function parseTable (node) {
  618. var txt = '',
  619. tableArray = [[], []],
  620. headings = node.querySelectorAll('thead>tr>th'),
  621. rows = node.querySelectorAll('tbody>tr'),
  622. i, ii;
  623. for (i = 0; i < headings.length; ++i) {
  624. var headContent = parseTableCell(headings[i]),
  625. allign = '---';
  626. if (headings[i].hasAttribute('style')) {
  627. var style = headings[i].getAttribute('style').toLowerCase().replace(/\s/g, '');
  628. switch (style) {
  629. case 'text-align:left;':
  630. allign = ':---';
  631. break;
  632. case 'text-align:right;':
  633. allign = '---:';
  634. break;
  635. case 'text-align:center;':
  636. allign = ':---:';
  637. break;
  638. }
  639. }
  640. tableArray[0][i] = headContent.trim();
  641. tableArray[1][i] = allign;
  642. }
  643. for (i = 0; i < rows.length; ++i) {
  644. var r = tableArray.push([]) - 1,
  645. cols = rows[i].getElementsByTagName('td');
  646. for (ii = 0; ii < headings.length; ++ii) {
  647. var cellContent = ' ';
  648. if (typeof cols[ii] !== 'undefined') {
  649. cellContent = parseTableCell(cols[ii]);
  650. }
  651. tableArray[r].push(cellContent);
  652. }
  653. }
  654. var cellSpacesCount = 3;
  655. for (i = 0; i < tableArray.length; ++i) {
  656. for (ii = 0; ii < tableArray[i].length; ++ii) {
  657. var strLen = tableArray[i][ii].length;
  658. if (strLen > cellSpacesCount) {
  659. cellSpacesCount = strLen;
  660. }
  661. }
  662. }
  663. for (i = 0; i < tableArray.length; ++i) {
  664. for (ii = 0; ii < tableArray[i].length; ++ii) {
  665. if (i === 1) {
  666. if (tableArray[i][ii].slice(-1) === ':') {
  667. tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii].slice(-1), cellSpacesCount - 1, '-') + ':';
  668. } else {
  669. tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount, '-');
  670. }
  671. } else {
  672. tableArray[i][ii] = showdown.helper.padEnd(tableArray[i][ii], cellSpacesCount);
  673. }
  674. }
  675. txt += '| ' + tableArray[i].join(' | ') + ' |\n';
  676. }
  677. return txt.trim();
  678. }
  679. function parseTableCell (node) {
  680. var txt = '';
  681. if (!node.hasChildNodes()) {
  682. return '';
  683. }
  684. var children = node.childNodes,
  685. childrenLength = children.length;
  686. for (var i = 0; i < childrenLength; ++i) {
  687. txt += parseNode(children[i], true);
  688. }
  689. return txt.trim();
  690. }
  691. function clean (node) {
  692. for (var n = 0; n < node.childNodes.length; ++n) {
  693. var child = node.childNodes[n];
  694. if (child.nodeType === 3) {
  695. if (!/\S/.test(child.nodeValue)) {
  696. node.removeChild(child);
  697. --n;
  698. } else {
  699. child.nodeValue = child.nodeValue.split('\n').join(' ');
  700. child.nodeValue = child.nodeValue.replace(/(\s)+/g, '$1');
  701. }
  702. } else if (child.nodeType === 1) {
  703. clean(child);
  704. }
  705. }
  706. }
  707. // find all pre tags and replace contents with placeholder
  708. // we need this so that we can remove all indentation from html
  709. // to ease up parsing
  710. function substitutePreCodeTags (doc) {
  711. var pres = doc.querySelectorAll('pre'),
  712. presPH = [];
  713. for (var i = 0; i < pres.length; ++i) {
  714. if (pres[i].childElementCount === 1 && pres[i].firstChild.tagName.toLowerCase() === 'code') {
  715. var content = pres[i].firstChild.innerHTML.trim(),
  716. language = pres[i].firstChild.getAttribute('data-language') || '';
  717. // if data-language attribute is not defined, then we look for class language-*
  718. if (language === '') {
  719. var classes = pres[i].firstChild.className.split(' ');
  720. for (var c = 0; c < classes.length; ++c) {
  721. var matches = classes[c].match(/^language-(.+)$/);
  722. if (matches !== null) {
  723. language = matches[1];
  724. break;
  725. }
  726. }
  727. }
  728. // unescape html entities in content
  729. content = showdown.helper.unescapeHTMLEntities(content);
  730. presPH.push(content);
  731. pres[i].outerHTML = '<precode language="' + language + '" precodenum="' + i.toString() + '"></precode>';
  732. } else {
  733. presPH.push(pres[i].innerHTML);
  734. pres[i].innerHTML = '';
  735. pres[i].setAttribute('prenum', i.toString());
  736. }
  737. }
  738. return presPH;
  739. }
  740. return mdDoc;
  741. };
  742. /**
  743. * Set an option of this Converter instance
  744. * @param {string} key
  745. * @param {*} value
  746. */
  747. this.setOption = function (key, value) {
  748. options[key] = value;
  749. };
  750. /**
  751. * Get the option of this Converter instance
  752. * @param {string} key
  753. * @returns {*}
  754. */
  755. this.getOption = function (key) {
  756. return options[key];
  757. };
  758. /**
  759. * Get the options of this Converter instance
  760. * @returns {{}}
  761. */
  762. this.getOptions = function () {
  763. return options;
  764. };
  765. /**
  766. * Add extension to THIS converter
  767. * @param {{}} extension
  768. * @param {string} [name=null]
  769. */
  770. this.addExtension = function (extension, name) {
  771. name = name || null;
  772. _parseExtension(extension, name);
  773. };
  774. /**
  775. * Use a global registered extension with THIS converter
  776. * @param {string} extensionName Name of the previously registered extension
  777. */
  778. this.useExtension = function (extensionName) {
  779. _parseExtension(extensionName);
  780. };
  781. /**
  782. * Set the flavor THIS converter should use
  783. * @param {string} name
  784. */
  785. this.setFlavor = function (name) {
  786. if (!flavor.hasOwnProperty(name)) {
  787. throw Error(name + ' flavor was not found');
  788. }
  789. var preset = flavor[name];
  790. setConvFlavor = name;
  791. for (var option in preset) {
  792. if (preset.hasOwnProperty(option)) {
  793. options[option] = preset[option];
  794. }
  795. }
  796. };
  797. /**
  798. * Get the currently set flavor of this converter
  799. * @returns {string}
  800. */
  801. this.getFlavor = function () {
  802. return setConvFlavor;
  803. };
  804. /**
  805. * Remove an extension from THIS converter.
  806. * Note: This is a costly operation. It's better to initialize a new converter
  807. * and specify the extensions you wish to use
  808. * @param {Array} extension
  809. */
  810. this.removeExtension = function (extension) {
  811. if (!showdown.helper.isArray(extension)) {
  812. extension = [extension];
  813. }
  814. for (var a = 0; a < extension.length; ++a) {
  815. var ext = extension[a];
  816. for (var i = 0; i < langExtensions.length; ++i) {
  817. if (langExtensions[i] === ext) {
  818. langExtensions[i].splice(i, 1);
  819. }
  820. }
  821. for (var ii = 0; ii < outputModifiers.length; ++i) {
  822. if (outputModifiers[ii] === ext) {
  823. outputModifiers[ii].splice(i, 1);
  824. }
  825. }
  826. }
  827. };
  828. /**
  829. * Get all extension of THIS converter
  830. * @returns {{language: Array, output: Array}}
  831. */
  832. this.getAllExtensions = function () {
  833. return {
  834. language: langExtensions,
  835. output: outputModifiers
  836. };
  837. };
  838. /**
  839. * Get the metadata of the previously parsed document
  840. * @param raw
  841. * @returns {string|{}}
  842. */
  843. this.getMetadata = function (raw) {
  844. if (raw) {
  845. return metadata.raw;
  846. } else {
  847. return metadata.parsed;
  848. }
  849. };
  850. /**
  851. * Get the metadata format of the previously parsed document
  852. * @returns {string}
  853. */
  854. this.getMetadataFormat = function () {
  855. return metadata.format;
  856. };
  857. /**
  858. * Private: set a single key, value metadata pair
  859. * @param {string} key
  860. * @param {string} value
  861. */
  862. this._setMetadataPair = function (key, value) {
  863. metadata.parsed[key] = value;
  864. };
  865. /**
  866. * Private: set metadata format
  867. * @param {string} format
  868. */
  869. this._setMetadataFormat = function (format) {
  870. metadata.format = format;
  871. };
  872. /**
  873. * Private: set metadata raw text
  874. * @param {string} raw
  875. */
  876. this._setMetadataRaw = function (raw) {
  877. metadata.raw = raw;
  878. };
  879. };