showdown.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. /**
  2. * Created by Tivie on 06-01-2015.
  3. */
  4. // Private properties
  5. var showdown = {},
  6. parsers = {},
  7. extensions = {},
  8. globalOptions = getDefaultOpts(true),
  9. setFlavor = 'vanilla',
  10. flavor = {
  11. github: {
  12. omitExtraWLInCodeBlocks: true,
  13. simplifiedAutoLink: true,
  14. excludeTrailingPunctuationFromURLs: true,
  15. literalMidWordUnderscores: true,
  16. strikethrough: true,
  17. tables: true,
  18. tablesHeaderId: true,
  19. ghCodeBlocks: true,
  20. tasklists: true,
  21. disableForced4SpacesIndentedSublists: true,
  22. simpleLineBreaks: true,
  23. requireSpaceBeforeHeadingText: true,
  24. ghCompatibleHeaderId: true,
  25. ghMentions: true,
  26. backslashEscapesHTMLTags: true
  27. },
  28. original: {
  29. noHeaderId: true,
  30. ghCodeBlocks: false
  31. },
  32. ghost: {
  33. omitExtraWLInCodeBlocks: true,
  34. parseImgDimensions: true,
  35. simplifiedAutoLink: true,
  36. excludeTrailingPunctuationFromURLs: true,
  37. literalMidWordUnderscores: true,
  38. strikethrough: true,
  39. tables: true,
  40. tablesHeaderId: true,
  41. ghCodeBlocks: true,
  42. tasklists: true,
  43. smoothLivePreview: true,
  44. simpleLineBreaks: true,
  45. requireSpaceBeforeHeadingText: true,
  46. ghMentions: false,
  47. encodeEmails: true
  48. },
  49. vanilla: getDefaultOpts(true),
  50. allOn: allOptionsOn()
  51. };
  52. /**
  53. * helper namespace
  54. * @type {{}}
  55. */
  56. showdown.helper = {};
  57. /**
  58. * TODO LEGACY SUPPORT CODE
  59. * @type {{}}
  60. */
  61. showdown.extensions = {};
  62. /**
  63. * Set a global option
  64. * @static
  65. * @param {string} key
  66. * @param {*} value
  67. * @returns {showdown}
  68. */
  69. showdown.setOption = function (key, value) {
  70. 'use strict';
  71. globalOptions[key] = value;
  72. return this;
  73. };
  74. /**
  75. * Get a global option
  76. * @static
  77. * @param {string} key
  78. * @returns {*}
  79. */
  80. showdown.getOption = function (key) {
  81. 'use strict';
  82. return globalOptions[key];
  83. };
  84. /**
  85. * Get the global options
  86. * @static
  87. * @returns {{}}
  88. */
  89. showdown.getOptions = function () {
  90. 'use strict';
  91. return globalOptions;
  92. };
  93. /**
  94. * Reset global options to the default values
  95. * @static
  96. */
  97. showdown.resetOptions = function () {
  98. 'use strict';
  99. globalOptions = getDefaultOpts(true);
  100. };
  101. /**
  102. * Set the flavor showdown should use as default
  103. * @param {string} name
  104. */
  105. showdown.setFlavor = function (name) {
  106. 'use strict';
  107. if (!flavor.hasOwnProperty(name)) {
  108. throw Error(name + ' flavor was not found');
  109. }
  110. showdown.resetOptions();
  111. var preset = flavor[name];
  112. setFlavor = name;
  113. for (var option in preset) {
  114. if (preset.hasOwnProperty(option)) {
  115. globalOptions[option] = preset[option];
  116. }
  117. }
  118. };
  119. /**
  120. * Get the currently set flavor
  121. * @returns {string}
  122. */
  123. showdown.getFlavor = function () {
  124. 'use strict';
  125. return setFlavor;
  126. };
  127. /**
  128. * Get the options of a specified flavor. Returns undefined if the flavor was not found
  129. * @param {string} name Name of the flavor
  130. * @returns {{}|undefined}
  131. */
  132. showdown.getFlavorOptions = function (name) {
  133. 'use strict';
  134. if (flavor.hasOwnProperty(name)) {
  135. return flavor[name];
  136. }
  137. };
  138. /**
  139. * Get the default options
  140. * @static
  141. * @param {boolean} [simple=true]
  142. * @returns {{}}
  143. */
  144. showdown.getDefaultOptions = function (simple) {
  145. 'use strict';
  146. return getDefaultOpts(simple);
  147. };
  148. /**
  149. * Get or set a subParser
  150. *
  151. * subParser(name) - Get a registered subParser
  152. * subParser(name, func) - Register a subParser
  153. * @static
  154. * @param {string} name
  155. * @param {function} [func]
  156. * @returns {*}
  157. */
  158. showdown.subParser = function (name, func) {
  159. 'use strict';
  160. if (showdown.helper.isString(name)) {
  161. if (typeof func !== 'undefined') {
  162. parsers[name] = func;
  163. } else {
  164. if (parsers.hasOwnProperty(name)) {
  165. return parsers[name];
  166. } else {
  167. throw Error('SubParser named ' + name + ' not registered!');
  168. }
  169. }
  170. }
  171. };
  172. /**
  173. * Gets or registers an extension
  174. * @static
  175. * @param {string} name
  176. * @param {object|function=} ext
  177. * @returns {*}
  178. */
  179. showdown.extension = function (name, ext) {
  180. 'use strict';
  181. if (!showdown.helper.isString(name)) {
  182. throw Error('Extension \'name\' must be a string');
  183. }
  184. name = showdown.helper.stdExtName(name);
  185. // Getter
  186. if (showdown.helper.isUndefined(ext)) {
  187. if (!extensions.hasOwnProperty(name)) {
  188. throw Error('Extension named ' + name + ' is not registered!');
  189. }
  190. return extensions[name];
  191. // Setter
  192. } else {
  193. // Expand extension if it's wrapped in a function
  194. if (typeof ext === 'function') {
  195. ext = ext();
  196. }
  197. // Ensure extension is an array
  198. if (!showdown.helper.isArray(ext)) {
  199. ext = [ext];
  200. }
  201. var validExtension = validate(ext, name);
  202. if (validExtension.valid) {
  203. extensions[name] = ext;
  204. } else {
  205. throw Error(validExtension.error);
  206. }
  207. }
  208. };
  209. /**
  210. * Gets all extensions registered
  211. * @returns {{}}
  212. */
  213. showdown.getAllExtensions = function () {
  214. 'use strict';
  215. return extensions;
  216. };
  217. /**
  218. * Remove an extension
  219. * @param {string} name
  220. */
  221. showdown.removeExtension = function (name) {
  222. 'use strict';
  223. delete extensions[name];
  224. };
  225. /**
  226. * Removes all extensions
  227. */
  228. showdown.resetExtensions = function () {
  229. 'use strict';
  230. extensions = {};
  231. };
  232. /**
  233. * Validate extension
  234. * @param {array} extension
  235. * @param {string} name
  236. * @returns {{valid: boolean, error: string}}
  237. */
  238. function validate (extension, name) {
  239. 'use strict';
  240. var errMsg = (name) ? 'Error in ' + name + ' extension->' : 'Error in unnamed extension',
  241. ret = {
  242. valid: true,
  243. error: ''
  244. };
  245. if (!showdown.helper.isArray(extension)) {
  246. extension = [extension];
  247. }
  248. for (var i = 0; i < extension.length; ++i) {
  249. var baseMsg = errMsg + ' sub-extension ' + i + ': ',
  250. ext = extension[i];
  251. if (typeof ext !== 'object') {
  252. ret.valid = false;
  253. ret.error = baseMsg + 'must be an object, but ' + typeof ext + ' given';
  254. return ret;
  255. }
  256. if (!showdown.helper.isString(ext.type)) {
  257. ret.valid = false;
  258. ret.error = baseMsg + 'property "type" must be a string, but ' + typeof ext.type + ' given';
  259. return ret;
  260. }
  261. var type = ext.type = ext.type.toLowerCase();
  262. // normalize extension type
  263. if (type === 'language') {
  264. type = ext.type = 'lang';
  265. }
  266. if (type === 'html') {
  267. type = ext.type = 'output';
  268. }
  269. if (type !== 'lang' && type !== 'output' && type !== 'listener') {
  270. ret.valid = false;
  271. ret.error = baseMsg + 'type ' + type + ' is not recognized. Valid values: "lang/language", "output/html" or "listener"';
  272. return ret;
  273. }
  274. if (type === 'listener') {
  275. if (showdown.helper.isUndefined(ext.listeners)) {
  276. ret.valid = false;
  277. ret.error = baseMsg + '. Extensions of type "listener" must have a property called "listeners"';
  278. return ret;
  279. }
  280. } else {
  281. if (showdown.helper.isUndefined(ext.filter) && showdown.helper.isUndefined(ext.regex)) {
  282. ret.valid = false;
  283. ret.error = baseMsg + type + ' extensions must define either a "regex" property or a "filter" method';
  284. return ret;
  285. }
  286. }
  287. if (ext.listeners) {
  288. if (typeof ext.listeners !== 'object') {
  289. ret.valid = false;
  290. ret.error = baseMsg + '"listeners" property must be an object but ' + typeof ext.listeners + ' given';
  291. return ret;
  292. }
  293. for (var ln in ext.listeners) {
  294. if (ext.listeners.hasOwnProperty(ln)) {
  295. if (typeof ext.listeners[ln] !== 'function') {
  296. ret.valid = false;
  297. ret.error = baseMsg + '"listeners" property must be an hash of [event name]: [callback]. listeners.' + ln +
  298. ' must be a function but ' + typeof ext.listeners[ln] + ' given';
  299. return ret;
  300. }
  301. }
  302. }
  303. }
  304. if (ext.filter) {
  305. if (typeof ext.filter !== 'function') {
  306. ret.valid = false;
  307. ret.error = baseMsg + '"filter" must be a function, but ' + typeof ext.filter + ' given';
  308. return ret;
  309. }
  310. } else if (ext.regex) {
  311. if (showdown.helper.isString(ext.regex)) {
  312. ext.regex = new RegExp(ext.regex, 'g');
  313. }
  314. if (!(ext.regex instanceof RegExp)) {
  315. ret.valid = false;
  316. ret.error = baseMsg + '"regex" property must either be a string or a RegExp object, but ' + typeof ext.regex + ' given';
  317. return ret;
  318. }
  319. if (showdown.helper.isUndefined(ext.replace)) {
  320. ret.valid = false;
  321. ret.error = baseMsg + '"regex" extensions must implement a replace string or function';
  322. return ret;
  323. }
  324. }
  325. }
  326. return ret;
  327. }
  328. /**
  329. * Validate extension
  330. * @param {object} ext
  331. * @returns {boolean}
  332. */
  333. showdown.validateExtension = function (ext) {
  334. 'use strict';
  335. var validateExtension = validate(ext, null);
  336. if (!validateExtension.valid) {
  337. console.warn(validateExtension.error);
  338. return false;
  339. }
  340. return true;
  341. };