converter.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  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. _constructor();
  42. /**
  43. * Converter constructor
  44. * @private
  45. */
  46. function _constructor () {
  47. converterOptions = converterOptions || {};
  48. for (var gOpt in globalOptions) {
  49. if (globalOptions.hasOwnProperty(gOpt)) {
  50. options[gOpt] = globalOptions[gOpt];
  51. }
  52. }
  53. // Merge options
  54. if (typeof converterOptions === 'object') {
  55. for (var opt in converterOptions) {
  56. if (converterOptions.hasOwnProperty(opt)) {
  57. options[opt] = converterOptions[opt];
  58. }
  59. }
  60. } else {
  61. throw Error('Converter expects the passed parameter to be an object, but ' + typeof converterOptions +
  62. ' was passed instead.');
  63. }
  64. if (options.extensions) {
  65. showdown.helper.forEach(options.extensions, _parseExtension);
  66. }
  67. }
  68. /**
  69. * Parse extension
  70. * @param {*} ext
  71. * @param {string} [name='']
  72. * @private
  73. */
  74. function _parseExtension (ext, name) {
  75. name = name || null;
  76. // If it's a string, the extension was previously loaded
  77. if (showdown.helper.isString(ext)) {
  78. ext = showdown.helper.stdExtName(ext);
  79. name = ext;
  80. // LEGACY_SUPPORT CODE
  81. if (showdown.extensions[ext]) {
  82. console.warn('DEPRECATION WARNING: ' + ext + ' is an old extension that uses a deprecated loading method.' +
  83. 'Please inform the developer that the extension should be updated!');
  84. legacyExtensionLoading(showdown.extensions[ext], ext);
  85. return;
  86. // END LEGACY SUPPORT CODE
  87. } else if (!showdown.helper.isUndefined(extensions[ext])) {
  88. ext = extensions[ext];
  89. } else {
  90. throw Error('Extension "' + ext + '" could not be loaded. It was either not found or is not a valid extension.');
  91. }
  92. }
  93. if (typeof ext === 'function') {
  94. ext = ext();
  95. }
  96. if (!showdown.helper.isArray(ext)) {
  97. ext = [ext];
  98. }
  99. var validExt = validate(ext, name);
  100. if (!validExt.valid) {
  101. throw Error(validExt.error);
  102. }
  103. for (var i = 0; i < ext.length; ++i) {
  104. switch (ext[i].type) {
  105. case 'lang':
  106. langExtensions.push(ext[i]);
  107. break;
  108. case 'output':
  109. outputModifiers.push(ext[i]);
  110. break;
  111. }
  112. if (ext[i].hasOwnProperty('listeners')) {
  113. for (var ln in ext[i].listeners) {
  114. if (ext[i].listeners.hasOwnProperty(ln)) {
  115. listen(ln, ext[i].listeners[ln]);
  116. }
  117. }
  118. }
  119. }
  120. }
  121. /**
  122. * LEGACY_SUPPORT
  123. * @param {*} ext
  124. * @param {string} name
  125. */
  126. function legacyExtensionLoading (ext, name) {
  127. if (typeof ext === 'function') {
  128. ext = ext(new showdown.Converter());
  129. }
  130. if (!showdown.helper.isArray(ext)) {
  131. ext = [ext];
  132. }
  133. var valid = validate(ext, name);
  134. if (!valid.valid) {
  135. throw Error(valid.error);
  136. }
  137. for (var i = 0; i < ext.length; ++i) {
  138. switch (ext[i].type) {
  139. case 'lang':
  140. langExtensions.push(ext[i]);
  141. break;
  142. case 'output':
  143. outputModifiers.push(ext[i]);
  144. break;
  145. default:// should never reach here
  146. throw Error('Extension loader error: Type unrecognized!!!');
  147. }
  148. }
  149. }
  150. /**
  151. * Listen to an event
  152. * @param {string} name
  153. * @param {function} callback
  154. */
  155. function listen (name, callback) {
  156. if (!showdown.helper.isString(name)) {
  157. throw Error('Invalid argument in converter.listen() method: name must be a string, but ' + typeof name + ' given');
  158. }
  159. if (typeof callback !== 'function') {
  160. throw Error('Invalid argument in converter.listen() method: callback must be a function, but ' + typeof callback + ' given');
  161. }
  162. if (!listeners.hasOwnProperty(name)) {
  163. listeners[name] = [];
  164. }
  165. listeners[name].push(callback);
  166. }
  167. function rTrimInputText (text) {
  168. var rsp = text.match(/^\s*/)[0].length,
  169. rgx = new RegExp('^\\s{0,' + rsp + '}', 'gm');
  170. return text.replace(rgx, '');
  171. }
  172. /**
  173. * Dispatch an event
  174. * @private
  175. * @param {string} evtName Event name
  176. * @param {string} text Text
  177. * @param {{}} options Converter Options
  178. * @param {{}} globals
  179. * @returns {string}
  180. */
  181. this._dispatch = function dispatch (evtName, text, options, globals) {
  182. if (listeners.hasOwnProperty(evtName)) {
  183. for (var ei = 0; ei < listeners[evtName].length; ++ei) {
  184. var nText = listeners[evtName][ei](evtName, text, this, options, globals);
  185. if (nText && typeof nText !== 'undefined') {
  186. text = nText;
  187. }
  188. }
  189. }
  190. return text;
  191. };
  192. /**
  193. * Listen to an event
  194. * @param {string} name
  195. * @param {function} callback
  196. * @returns {showdown.Converter}
  197. */
  198. this.listen = function (name, callback) {
  199. listen(name, callback);
  200. return this;
  201. };
  202. /**
  203. * Converts a markdown string into HTML
  204. * @param {string} text
  205. * @returns {*}
  206. */
  207. this.makeHtml = function (text) {
  208. //check if text is not falsy
  209. if (!text) {
  210. return text;
  211. }
  212. var globals = {
  213. gHtmlBlocks: [],
  214. gHtmlMdBlocks: [],
  215. gHtmlSpans: [],
  216. gUrls: {},
  217. gTitles: {},
  218. gDimensions: {},
  219. gListLevel: 0,
  220. hashLinkCounts: {},
  221. langExtensions: langExtensions,
  222. outputModifiers: outputModifiers,
  223. converter: this,
  224. ghCodeBlocks: []
  225. };
  226. // This lets us use ¨ trema as an escape char to avoid md5 hashes
  227. // The choice of character is arbitrary; anything that isn't
  228. // magic in Markdown will work.
  229. text = text.replace(/¨/g, '¨T');
  230. // Replace $ with ¨D
  231. // RegExp interprets $ as a special character
  232. // when it's in a replacement string
  233. text = text.replace(/\$/g, '¨D');
  234. // Standardize line endings
  235. text = text.replace(/\r\n/g, '\n'); // DOS to Unix
  236. text = text.replace(/\r/g, '\n'); // Mac to Unix
  237. // Stardardize line spaces (nbsp causes trouble in older browsers and some regex flavors)
  238. text = text.replace(/\u00A0/g, ' ');
  239. if (options.smartIndentationFix) {
  240. text = rTrimInputText(text);
  241. }
  242. // Make sure text begins and ends with a couple of newlines:
  243. text = '\n\n' + text + '\n\n';
  244. // detab
  245. text = showdown.subParser('detab')(text, options, globals);
  246. /**
  247. * Strip any lines consisting only of spaces and tabs.
  248. * This makes subsequent regexs easier to write, because we can
  249. * match consecutive blank lines with /\n+/ instead of something
  250. * contorted like /[ \t]*\n+/
  251. */
  252. text = text.replace(/^[ \t]+$/mg, '');
  253. //run languageExtensions
  254. showdown.helper.forEach(langExtensions, function (ext) {
  255. text = showdown.subParser('runExtension')(ext, text, options, globals);
  256. });
  257. // run the sub parsers
  258. text = showdown.subParser('hashPreCodeTags')(text, options, globals);
  259. text = showdown.subParser('githubCodeBlocks')(text, options, globals);
  260. text = showdown.subParser('hashHTMLBlocks')(text, options, globals);
  261. text = showdown.subParser('hashHTMLSpans')(text, options, globals);
  262. text = showdown.subParser('stripLinkDefinitions')(text, options, globals);
  263. text = showdown.subParser('blockGamut')(text, options, globals);
  264. text = showdown.subParser('unhashHTMLSpans')(text, options, globals);
  265. text = showdown.subParser('unescapeSpecialChars')(text, options, globals);
  266. // attacklab: Restore dollar signs
  267. text = text.replace(/¨D/g, '$$');
  268. // attacklab: Restore tremas
  269. text = text.replace(/¨T/g, '¨');
  270. // Run output modifiers
  271. showdown.helper.forEach(outputModifiers, function (ext) {
  272. text = showdown.subParser('runExtension')(ext, text, options, globals);
  273. });
  274. return text;
  275. };
  276. /**
  277. * Set an option of this Converter instance
  278. * @param {string} key
  279. * @param {*} value
  280. */
  281. this.setOption = function (key, value) {
  282. options[key] = value;
  283. };
  284. /**
  285. * Get the option of this Converter instance
  286. * @param {string} key
  287. * @returns {*}
  288. */
  289. this.getOption = function (key) {
  290. return options[key];
  291. };
  292. /**
  293. * Get the options of this Converter instance
  294. * @returns {{}}
  295. */
  296. this.getOptions = function () {
  297. return options;
  298. };
  299. /**
  300. * Add extension to THIS converter
  301. * @param {{}} extension
  302. * @param {string} [name=null]
  303. */
  304. this.addExtension = function (extension, name) {
  305. name = name || null;
  306. _parseExtension(extension, name);
  307. };
  308. /**
  309. * Use a global registered extension with THIS converter
  310. * @param {string} extensionName Name of the previously registered extension
  311. */
  312. this.useExtension = function (extensionName) {
  313. _parseExtension(extensionName);
  314. };
  315. /**
  316. * Set the flavor THIS converter should use
  317. * @param {string} name
  318. */
  319. this.setFlavor = function (name) {
  320. if (!flavor.hasOwnProperty(name)) {
  321. throw Error(name + ' flavor was not found');
  322. }
  323. var preset = flavor[name];
  324. setConvFlavor = name;
  325. for (var option in preset) {
  326. if (preset.hasOwnProperty(option)) {
  327. options[option] = preset[option];
  328. }
  329. }
  330. };
  331. /**
  332. * Get the currently set flavor of this converter
  333. * @returns {string}
  334. */
  335. this.getFlavor = function () {
  336. return setConvFlavor;
  337. };
  338. /**
  339. * Remove an extension from THIS converter.
  340. * Note: This is a costly operation. It's better to initialize a new converter
  341. * and specify the extensions you wish to use
  342. * @param {Array} extension
  343. */
  344. this.removeExtension = function (extension) {
  345. if (!showdown.helper.isArray(extension)) {
  346. extension = [extension];
  347. }
  348. for (var a = 0; a < extension.length; ++a) {
  349. var ext = extension[a];
  350. for (var i = 0; i < langExtensions.length; ++i) {
  351. if (langExtensions[i] === ext) {
  352. langExtensions[i].splice(i, 1);
  353. }
  354. }
  355. for (var ii = 0; ii < outputModifiers.length; ++i) {
  356. if (outputModifiers[ii] === ext) {
  357. outputModifiers[ii].splice(i, 1);
  358. }
  359. }
  360. }
  361. };
  362. /**
  363. * Get all extension of THIS converter
  364. * @returns {{language: Array, output: Array}}
  365. */
  366. this.getAllExtensions = function () {
  367. return {
  368. language: langExtensions,
  369. output: outputModifiers
  370. };
  371. };
  372. };