markdown.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. /**
  2. * Provides access to Markdown-related functions.
  3. * @module jsdoc/util/markdown
  4. */
  5. const env = require('jsdoc/env');
  6. const logger = require('jsdoc/util/logger');
  7. const MarkdownIt = require('markdown-it');
  8. const { marked } = require('marked');
  9. const mda = require('markdown-it-anchor');
  10. const path = require('jsdoc/path');
  11. const util = require('util');
  12. /**
  13. * Enumeration of Markdown parsers that are available.
  14. * @enum {String}
  15. */
  16. const parserNames = {
  17. /**
  18. * The [`markdown-js`](https://github.com/evilstreak/markdown-js) (aka "evilstreak") parser.
  19. *
  20. * @deprecated Replaced by `markdown-it`.
  21. */
  22. evilstreak: 'markdownit',
  23. /**
  24. * The "GitHub-flavored Markdown" parser.
  25. *
  26. * @deprecated Replaced by `markdown-it`.
  27. */
  28. gfm: 'markdownit',
  29. /**
  30. * The `markdown-it` parser.
  31. */
  32. markdownit: 'markdownit',
  33. /**
  34. * The [Marked](https://github.com/chjj/marked) parser.
  35. *
  36. * @deprecated Will be replaced by `markdown-it` in JSDoc 3.7.0.
  37. */
  38. marked: 'marked'
  39. };
  40. /**
  41. * Escape underscores that occur within an inline tag in order to protect them from the `marked`
  42. * parser.
  43. *
  44. * @param {string} source - The source text to sanitize.
  45. * @return {string} The source text, where underscores within inline tags have been protected with a
  46. * preceding backslash (e.g., `\_`). The `marked` parser will strip the backslash and protect the
  47. * underscore.
  48. */
  49. function escapeUnderscores(source) {
  50. return source.replace(/\{@[^}\r\n]+\}/g, wholeMatch => wholeMatch.replace(/(^|[^\\])_/g, '$1\\_'));
  51. }
  52. /**
  53. * Escape HTTP/HTTPS URLs so that they are not automatically converted to HTML links.
  54. *
  55. * @param {string} source - The source text to escape.
  56. * @return {string} The source text with escape characters added to HTTP/HTTPS URLs.
  57. */
  58. function escapeUrls(source) {
  59. return source.replace(/(https?):\/\//g, '$1:\\/\\/');
  60. }
  61. /**
  62. * Unescape HTTP/HTTPS URLs after Markdown parsing is complete.
  63. *
  64. * @param {string} source - The source text to unescape.
  65. * @return {string} The source text with escape characters removed from HTTP/HTTPS URLs.
  66. */
  67. function unescapeUrls(source) {
  68. return source.replace(/(https?):\\\/\\\//g, '$1://');
  69. }
  70. /**
  71. * Escape backslashes within inline tags so that they are not stripped.
  72. *
  73. * @param {string} source - The source text to escape.
  74. * @return {string} The source text with backslashes escaped within inline tags.
  75. */
  76. function escapeInlineTagBackslashes(source) {
  77. return source.replace(/\{@[^}\r\n]+\}/g, wholeMatch => wholeMatch.replace(/\\/g, '\\\\'));
  78. }
  79. /**
  80. * Escape characters in text within a code block.
  81. *
  82. * @param {string} source - The source text to escape.
  83. * @return {string} The escaped source text.
  84. */
  85. function escapeCode(source) {
  86. return source.replace(/</g, '&lt;')
  87. .replace(/"/g, '&quot;')
  88. .replace(/'/g, '&#39;');
  89. }
  90. /**
  91. * Wrap a code snippet in HTML tags that enable syntax highlighting.
  92. *
  93. * @param {string} code - The code snippet.
  94. * @param {string?} language - The language of the code snippet.
  95. * @return {string} The wrapped code snippet.
  96. */
  97. function highlight(code, language) {
  98. let classString;
  99. let langClass = '';
  100. if (language && (language !== 'plain')) {
  101. langClass = ` lang-${language}`;
  102. }
  103. if (language !== 'plain') {
  104. classString = util.format(' class="prettyprint source%s"', langClass);
  105. }
  106. else {
  107. classString = ' class="source"';
  108. }
  109. return util.format('<pre%s><code>%s</code></pre>', classString, escapeCode(code));
  110. }
  111. /**
  112. * Unencode quotes that occur within {@ ... } after the Markdown parser has turned them into HTML
  113. * entities.
  114. *
  115. * @param {string} source - The source text to unencode.
  116. * @return {string} The source text with HTML entity `&quot;` converted back to standard quotes.
  117. */
  118. function unencodeQuotes(source) {
  119. return source.replace(/\{@[^}\r\n]+\}/g, wholeMatch => wholeMatch.replace(/&quot;/g, '"'));
  120. }
  121. /**
  122. * Get the appropriate function for applying syntax highlighting to text, based on the user's
  123. * Markdown configuration settings.
  124. *
  125. * @param {Object} conf - The user's Markdown configuration settings.
  126. * @return {function} The highlighter function.
  127. */
  128. function getHighlighter(conf) {
  129. let highlighter;
  130. let highlighterPath;
  131. switch (typeof conf.highlight) {
  132. case 'string':
  133. highlighterPath = path.getResourcePath(conf.highlight);
  134. if (highlighterPath) {
  135. highlighter = require(highlighterPath).highlight;
  136. if (typeof highlighter !== 'function') {
  137. logger.error('The syntax highlighting module "%s" does not assign a method ' +
  138. 'to exports.highlight. Using the default syntax highlighter.',
  139. conf.highlight);
  140. highlighter = highlight;
  141. }
  142. }
  143. else {
  144. logger.error('Unable to find the syntax highlighting module "%s". Using the ' +
  145. 'default syntax highlighter.', conf.highlight);
  146. highlighter = highlight;
  147. }
  148. break;
  149. case 'function':
  150. highlighter = conf.highlight;
  151. break;
  152. default:
  153. highlighter = highlight;
  154. }
  155. return highlighter;
  156. }
  157. /**
  158. * Retrieve a function that accepts a single parameter containing Markdown source. The function uses
  159. * the specified parser to transform the Markdown source to HTML, then returns the HTML as a string.
  160. *
  161. * @private
  162. * @param {String} parserName The name of the selected parser.
  163. * @param {Object} [conf] Configuration for the selected parser, if any.
  164. * @returns {Function} A function that accepts Markdown source, feeds it to the selected parser, and
  165. * returns the resulting HTML.
  166. */
  167. function getParseFunction(parserName, conf) {
  168. let highlighter;
  169. let parserFunction;
  170. let renderer;
  171. conf = conf || {};
  172. highlighter = getHighlighter(conf);
  173. switch (parserName) {
  174. case parserNames.marked:
  175. if (conf.hardwrap) {
  176. marked.setOptions({breaks: true});
  177. }
  178. // Marked generates an "id" attribute for headers; this custom renderer suppresses it
  179. renderer = new marked.Renderer();
  180. if (!conf.idInHeadings) {
  181. renderer.heading = (text, level) => util.format('<h%s>%s</h%s>', level, text, level);
  182. }
  183. renderer.code = highlighter;
  184. parserFunction = source => {
  185. let result;
  186. source = escapeUnderscores(source);
  187. source = escapeUrls(source);
  188. result = marked(source, { renderer: renderer })
  189. .replace(/\s+$/, '')
  190. .replace(/&#39;/g, "'");
  191. result = unescapeUrls(result);
  192. result = unencodeQuotes(result);
  193. return result;
  194. };
  195. parserFunction._parser = parserNames.marked;
  196. return parserFunction;
  197. case parserNames.markdownit:
  198. renderer = new MarkdownIt({
  199. breaks: Boolean(conf.hardwrap),
  200. highlight: highlighter,
  201. html: true
  202. });
  203. if (conf.idInHeadings) {
  204. renderer.use(mda, { tabIndex: false });
  205. }
  206. parserFunction = source => {
  207. let result;
  208. source = escapeUrls(source);
  209. source = escapeInlineTagBackslashes(source);
  210. result = renderer.render(source)
  211. .replace(/\s+$/, '')
  212. .replace(/&#39;/g, "'");
  213. result = unescapeUrls(result);
  214. result = unencodeQuotes(result);
  215. return result;
  216. };
  217. parserFunction._parser = parserNames.markdownit;
  218. return parserFunction;
  219. default:
  220. logger.error('Unrecognized Markdown parser "%s". Markdown support is disabled.',
  221. parserName);
  222. return undefined;
  223. }
  224. }
  225. /**
  226. * Retrieve a Markdown parsing function based on the value of the `conf.json` file's
  227. * `env.conf.markdown` property. The parsing function accepts a single parameter containing Markdown
  228. * source. The function uses the parser specified in `conf.json` to transform the Markdown source to
  229. * HTML, then returns the HTML as a string.
  230. *
  231. * @returns {function} A function that accepts Markdown source, feeds it to the selected parser, and
  232. * returns the resulting HTML.
  233. */
  234. exports.getParser = () => {
  235. const conf = env.conf.markdown;
  236. const parser = (conf && conf.parser) ? parserNames[conf.parser] : parserNames.markdownit;
  237. return getParseFunction(parser, conf);
  238. };