Edit File: markdown.js
/** * Provides access to Markdown-related functions. * @module jsdoc/util/markdown */ const env = require('jsdoc/env'); const logger = require('jsdoc/util/logger'); const MarkdownIt = require('markdown-it'); const { marked } = require('marked'); const mda = require('markdown-it-anchor'); const path = require('jsdoc/path'); const util = require('util'); /** * Enumeration of Markdown parsers that are available. * @enum {String} */ const parserNames = { /** * The [`markdown-js`](https://github.com/evilstreak/markdown-js) (aka "evilstreak") parser. * * @deprecated Replaced by `markdown-it`. */ evilstreak: 'markdownit', /** * The "GitHub-flavored Markdown" parser. * * @deprecated Replaced by `markdown-it`. */ gfm: 'markdownit', /** * The `markdown-it` parser. */ markdownit: 'markdownit', /** * The [Marked](https://github.com/chjj/marked) parser. * * @deprecated Will be replaced by `markdown-it` in JSDoc 3.7.0. */ marked: 'marked' }; /** * Escape underscores that occur within an inline tag in order to protect them from the `marked` * parser. * * @param {string} source - The source text to sanitize. * @return {string} The source text, where underscores within inline tags have been protected with a * preceding backslash (e.g., `\_`). The `marked` parser will strip the backslash and protect the * underscore. */ function escapeUnderscores(source) { return source.replace(/\{@[^}\r\n]+\}/g, wholeMatch => wholeMatch.replace(/(^|[^\\])_/g, '$1\\_')); } /** * Escape HTTP/HTTPS URLs so that they are not automatically converted to HTML links. * * @param {string} source - The source text to escape. * @return {string} The source text with escape characters added to HTTP/HTTPS URLs. */ function escapeUrls(source) { return source.replace(/(https?):\/\//g, '$1:\\/\\/'); } /** * Unescape HTTP/HTTPS URLs after Markdown parsing is complete. * * @param {string} source - The source text to unescape. * @return {string} The source text with escape characters removed from HTTP/HTTPS URLs. */ function unescapeUrls(source) { return source.replace(/(https?):\\\/\\\//g, '$1://'); } /** * Escape backslashes within inline tags so that they are not stripped. * * @param {string} source - The source text to escape. * @return {string} The source text with backslashes escaped within inline tags. */ function escapeInlineTagBackslashes(source) { return source.replace(/\{@[^}\r\n]+\}/g, wholeMatch => wholeMatch.replace(/\\/g, '\\\\')); } /** * Escape characters in text within a code block. * * @param {string} source - The source text to escape. * @return {string} The escaped source text. */ function escapeCode(source) { return source.replace(/</g, '<') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Wrap a code snippet in HTML tags that enable syntax highlighting. * * @param {string} code - The code snippet. * @param {string?} language - The language of the code snippet. * @return {string} The wrapped code snippet. */ function highlight(code, language) { let classString; let langClass = ''; if (language && (language !== 'plain')) { langClass = ` lang-${language}`; } if (language !== 'plain') { classString = util.format(' class="prettyprint source%s"', langClass); } else { classString = ' class="source"'; } return util.format('<pre%s><code>%s</code></pre>', classString, escapeCode(code)); } /** * Unencode quotes that occur within {@ ... } after the Markdown parser has turned them into HTML * entities. * * @param {string} source - The source text to unencode. * @return {string} The source text with HTML entity `"` converted back to standard quotes. */ function unencodeQuotes(source) { return source.replace(/\{@[^}\r\n]+\}/g, wholeMatch => wholeMatch.replace(/"/g, '"')); } /** * Get the appropriate function for applying syntax highlighting to text, based on the user's * Markdown configuration settings. * * @param {Object} conf - The user's Markdown configuration settings. * @return {function} The highlighter function. */ function getHighlighter(conf) { let highlighter; let highlighterPath; switch (typeof conf.highlight) { case 'string': highlighterPath = path.getResourcePath(conf.highlight); if (highlighterPath) { highlighter = require(highlighterPath).highlight; if (typeof highlighter !== 'function') { logger.error('The syntax highlighting module "%s" does not assign a method ' + 'to exports.highlight. Using the default syntax highlighter.', conf.highlight); highlighter = highlight; } } else { logger.error('Unable to find the syntax highlighting module "%s". Using the ' + 'default syntax highlighter.', conf.highlight); highlighter = highlight; } break; case 'function': highlighter = conf.highlight; break; default: highlighter = highlight; } return highlighter; } /** * Retrieve a function that accepts a single parameter containing Markdown source. The function uses * the specified parser to transform the Markdown source to HTML, then returns the HTML as a string. * * @private * @param {String} parserName The name of the selected parser. * @param {Object} [conf] Configuration for the selected parser, if any. * @returns {Function} A function that accepts Markdown source, feeds it to the selected parser, and * returns the resulting HTML. */ function getParseFunction(parserName, conf) { let highlighter; let parserFunction; let renderer; conf = conf || {}; highlighter = getHighlighter(conf); switch (parserName) { case parserNames.marked: if (conf.hardwrap) { marked.setOptions({breaks: true}); } // Marked generates an "id" attribute for headers; this custom renderer suppresses it renderer = new marked.Renderer(); if (!conf.idInHeadings) { renderer.heading = (text, level) => util.format('<h%s>%s</h%s>', level, text, level); } renderer.code = highlighter; parserFunction = source => { let result; source = escapeUnderscores(source); source = escapeUrls(source); result = marked(source, { renderer: renderer }) .replace(/\s+$/, '') .replace(/'/g, "'"); result = unescapeUrls(result); result = unencodeQuotes(result); return result; }; parserFunction._parser = parserNames.marked; return parserFunction; case parserNames.markdownit: renderer = new MarkdownIt({ breaks: Boolean(conf.hardwrap), highlight: highlighter, html: true }); if (conf.idInHeadings) { renderer.use(mda, { tabIndex: false }); } parserFunction = source => { let result; source = escapeUrls(source); source = escapeInlineTagBackslashes(source); result = renderer.render(source) .replace(/\s+$/, '') .replace(/'/g, "'"); result = unescapeUrls(result); result = unencodeQuotes(result); return result; }; parserFunction._parser = parserNames.markdownit; return parserFunction; default: logger.error('Unrecognized Markdown parser "%s". Markdown support is disabled.', parserName); return undefined; } } /** * Retrieve a Markdown parsing function based on the value of the `conf.json` file's * `env.conf.markdown` property. The parsing function accepts a single parameter containing Markdown * source. The function uses the parser specified in `conf.json` to transform the Markdown source to * HTML, then returns the HTML as a string. * * @returns {function} A function that accepts Markdown source, feeds it to the selected parser, and * returns the resulting HTML. */ exports.getParser = () => { const conf = env.conf.markdown; const parser = (conf && conf.parser) ? parserNames[conf.parser] : parserNames.markdownit; return getParseFunction(parser, conf); };
Back to File Manager