converter.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. /**
  2. * Created by Estevao on 31-05-2015.
  3. */
  4. /**
  5. * Showdown Converter class
  6. * @class
  7. * @param {object} [converterOptions]
  8. * @returns {
  9. * {makeHtml: Function},
  10. * {setOption: Function},
  11. * {getOption: Function},
  12. * {getOptions: Function}
  13. * }
  14. */
  15. showdown.Converter = function (converterOptions) {
  16. 'use strict';
  17. var
  18. /**
  19. * Options used by this converter
  20. * @private
  21. * @type {{}}
  22. */
  23. options = {
  24. omitExtraWLInCodeBlocks: false,
  25. prefixHeaderId: false,
  26. noHeaderId: false
  27. },
  28. /**
  29. * Language extensions used by this converter
  30. * @private
  31. * @type {Array}
  32. */
  33. langExtensions = [],
  34. /**
  35. * Output modifiers extensions used by this converter
  36. * @private
  37. * @type {Array}
  38. */
  39. outputModifiers = [],
  40. /**
  41. * The parser Order
  42. * @private
  43. * @type {string[]}
  44. */
  45. parserOrder = [
  46. 'githubCodeBlocks',
  47. 'hashHTMLBlocks',
  48. 'stripLinkDefinitions',
  49. 'blockGamut',
  50. 'unescapeSpecialChars'
  51. ];
  52. _constructor();
  53. /**
  54. * Converter constructor
  55. * @private
  56. */
  57. function _constructor() {
  58. converterOptions = converterOptions || {};
  59. for (var gOpt in globalOptions) {
  60. if (globalOptions.hasOwnProperty(gOpt)) {
  61. options[gOpt] = globalOptions[gOpt];
  62. }
  63. }
  64. // Merge options
  65. if (typeof converterOptions === 'object') {
  66. for (var opt in converterOptions) {
  67. if (converterOptions.hasOwnProperty(opt)) {
  68. options[opt] = converterOptions[opt];
  69. }
  70. }
  71. } else {
  72. throw Error('Converter expects the passed parameter to be an object, but ' + typeof converterOptions +
  73. ' was passed instead.');
  74. }
  75. if (options.extensions) {
  76. showdown.helper.forEach(options.extensions, _parseExtension);
  77. }
  78. }
  79. /**
  80. * Parse extension
  81. * @param {*} ext
  82. * @private
  83. */
  84. function _parseExtension(ext) {
  85. // If it's a string, the extension was previously loaded
  86. if (showdown.helper.isString(ext)) {
  87. ext = showdown.helper.stdExtName(ext);
  88. // LEGACY_SUPPORT CODE
  89. if (showdown.extensions[ext]) {
  90. console.warn('DEPRECATION WARNING: ' + ext + ' is an old extension that uses a deprecated loading method.' +
  91. 'Please inform the developer that the extension should be updated!');
  92. legacyExtensionLoading(showdown.extensions[ext], ext);
  93. return;
  94. // END LEGACY SUPPORT CODE
  95. } else if (!showdown.helper.isUndefined(extensions[ext])) {
  96. ext = extensions[ext];
  97. } else {
  98. throw Error('Extension "' + ext + '" could not be loaded. It was either not found or is not a valid extension.');
  99. }
  100. }
  101. if (typeof ext === 'function') {
  102. ext = ext();
  103. }
  104. if (!showdown.helper.isArray(ext)) {
  105. ext = [ext];
  106. }
  107. if (!showdown.validateExtension(ext)) {
  108. return;
  109. }
  110. for (var i = 0; i < ext.length; ++i) {
  111. switch (ext[i].type) {
  112. case 'lang':
  113. langExtensions.push(ext[i]);
  114. break;
  115. case 'output':
  116. outputModifiers.push(ext[i]);
  117. break;
  118. default:
  119. // should never reach here
  120. throw Error('Extension loader error: Type unrecognized!!!');
  121. }
  122. }
  123. }
  124. /**
  125. * LEGACY_SUPPORT
  126. * @param {*} ext
  127. * @param {string} name
  128. */
  129. function legacyExtensionLoading(ext, name) {
  130. if (typeof ext === 'function') {
  131. ext = ext(new showdown.Converter());
  132. }
  133. if (!showdown.helper.isArray(ext)) {
  134. ext = [ext];
  135. }
  136. var valid = validate(ext, name);
  137. if (!valid.valid) {
  138. throw Error(valid.error);
  139. }
  140. for (var i = 0; i < ext.length; ++i) {
  141. switch (ext[i].type) {
  142. case 'lang':
  143. langExtensions.push(ext[i]);
  144. break;
  145. case 'output':
  146. outputModifiers.push(ext[i]);
  147. break;
  148. default:// should never reach here
  149. throw Error('Extension loader error: Type unrecognized!!!');
  150. }
  151. }
  152. }
  153. /**
  154. * Converts a markdown string into HTML
  155. * @param {string} text
  156. * @returns {*}
  157. */
  158. this.makeHtml = function (text) {
  159. //check if text is not falsy
  160. if (!text) {
  161. return text;
  162. }
  163. var globals = {
  164. gHtmlBlocks: [],
  165. gUrls: {},
  166. gTitles: {},
  167. gDimensions: {},
  168. gListLevel: 0,
  169. hashLinkCounts: {},
  170. langExtensions: langExtensions,
  171. outputModifiers: outputModifiers,
  172. converter: this
  173. };
  174. // attacklab: Replace ~ with ~T
  175. // This lets us use tilde as an escape char to avoid md5 hashes
  176. // The choice of character is arbitrary; anything that isn't
  177. // magic in Markdown will work.
  178. text = text.replace(/~/g, '~T');
  179. // attacklab: Replace $ with ~D
  180. // RegExp interprets $ as a special character
  181. // when it's in a replacement string
  182. text = text.replace(/\$/g, '~D');
  183. // Standardize line endings
  184. text = text.replace(/\r\n/g, '\n'); // DOS to Unix
  185. text = text.replace(/\r/g, '\n'); // Mac to Unix
  186. // Make sure text begins and ends with a couple of newlines:
  187. text = '\n\n' + text + '\n\n';
  188. // detab
  189. text = showdown.subParser('detab')(text, options, globals);
  190. // stripBlankLines
  191. text = showdown.subParser('stripBlankLines')(text, options, globals);
  192. //run languageExtensions
  193. showdown.helper.forEach(langExtensions, function (ext) {
  194. text = showdown.subParser('runExtension')(ext, text, options, globals);
  195. });
  196. // Run all registered parsers
  197. for (var i = 0; i < parserOrder.length; ++i) {
  198. var name = parserOrder[i];
  199. text = parsers[name](text, options, globals);
  200. }
  201. // attacklab: Restore dollar signs
  202. text = text.replace(/~D/g, '$$');
  203. // attacklab: Restore tildes
  204. text = text.replace(/~T/g, '~');
  205. // Run output modifiers
  206. showdown.helper.forEach(outputModifiers, function (ext) {
  207. text = showdown.subParser('runExtension')(ext, text, options, globals);
  208. });
  209. return text;
  210. };
  211. /**
  212. * Set an option of this Converter instance
  213. * @param {string} key
  214. * @param {*} value
  215. */
  216. this.setOption = function (key, value) {
  217. options[key] = value;
  218. };
  219. /**
  220. * Get the option of this Converter instance
  221. * @param {string} key
  222. * @returns {*}
  223. */
  224. this.getOption = function (key) {
  225. return options[key];
  226. };
  227. /**
  228. * Get the options of this Converter instance
  229. * @returns {{}}
  230. */
  231. this.getOptions = function () {
  232. return options;
  233. };
  234. /**
  235. * Add extension to THIS converter
  236. * @param {{}} extension
  237. */
  238. this.addExtension = function (extension) {
  239. _parseExtension(extension);
  240. };
  241. /**
  242. * Use a global registered extension with THIS converter
  243. * @param {string} extensionName Name of the previously registered extension
  244. */
  245. this.useExtension = function (extensionName) {
  246. _parseExtension(extensionName);
  247. };
  248. /**
  249. * Remove an extension from THIS converter.
  250. * Note: This is a costly operation. It's better to initialize a new converter
  251. * and specify the extensions you wish to use
  252. * @param {Array} extension
  253. */
  254. this.removeExtension = function (extension) {
  255. if (!showdown.helper.isArray(extension)) {
  256. extension = [extension];
  257. }
  258. for (var a = 0; a < extension.length; ++a) {
  259. var ext = extension[a];
  260. for (var i = 0; i < langExtensions.length; ++i) {
  261. if (langExtensions[i] === ext) {
  262. langExtensions[i].splice(i, 1);
  263. }
  264. }
  265. for (var ii = 0; ii < outputModifiers.length; ++i) {
  266. if (outputModifiers[ii] === ext) {
  267. outputModifiers[ii].splice(i, 1);
  268. }
  269. }
  270. }
  271. };
  272. /**
  273. * Get all extension of THIS converter
  274. * @returns {{language: Array, output: Array}}
  275. */
  276. this.getAllExtensions = function () {
  277. return {
  278. language: langExtensions,
  279. output: outputModifiers
  280. };
  281. };
  282. };