'use strict';
const { marked } = require('marked');
let JSDOM,
createDOMPurify;
const { encodeURL, slugize, stripHTML, url_for, isExternalLink, escapeHTML: escape } = require('hexo-util');
const MarkedRenderer = marked.Renderer;
const MarkedTokenizer = marked.Tokenizer;
const { basename, dirname, extname, join } = require('path').posix;
const rATag = /]+\s+?)?href=["'](?:#)([^<>"']+)["'][^<>]*>/i;
const rDlSyntax = /(?:^|\s)(\S.+)
:\s+(\S.+)/;
const anchorId = (str, transformOption) => {
return slugize(str.trim(), {transform: transformOption});
};
class Renderer extends MarkedRenderer {
constructor(hexo) {
super();
this._headingId = {};
this.hexo = hexo;
}
// Add id attribute to headings
heading(text, level) {
const { anchorAlias, headerIds, modifyAnchors } = this.options;
const { _headingId } = this;
if (!headerIds) {
return `
${text}
\n`; } // Prepend root to image path image(href, title, text) { const { hexo, options } = this; const { relative_link } = hexo.config; const { lazyload, prependRoot, postPath } = options; if (!/^(#|\/\/|http(s)?:)/.test(href) && !relative_link && prependRoot) { if (!href.startsWith('/') && !href.startsWith('\\') && postPath) { const PostAsset = hexo.model('PostAsset'); // findById requires forward slash const asset = PostAsset.findById(join(postPath, href.replace(/\\/g, '/'))); // asset.path is backward slash in Windows if (asset) href = asset.path.replace(/\\/g, '/'); } href = url_for.call(hexo, href); } let out = `'; return out; } } marked.setOptions({ langPrefix: '' }); // https://github.com/markedjs/marked/blob/b6773fca412c339e0cedd56b63f9fa1583cfd372/src/Lexer.js#L8-L24 const smartypants = (str, quotes) => { const [openDbl, closeDbl, openSgl, closeSgl] = typeof quotes === 'string' && quotes.length === 4 ? quotes : ['\u201c', '\u201d', '\u2018', '\u2019']; return str // em-dashes .replace(/---/g, '\u2014') // en-dashes .replace(/--/g, '\u2013') // opening singles .replace(/(^|[-\u2014/([{"\s])'/g, '$1' + openSgl) // closing singles & apostrophes .replace(/'/g, closeSgl) // opening doubles .replace(/(^|[-\u2014/([{\u2018\s])"/g, '$1' + openDbl) // closing doubles .replace(/"/g, closeDbl) // ellipses .replace(/\.{3}/g, '\u2026'); }; class Tokenizer extends MarkedTokenizer { // Support AutoLink option url(src, mangle) { const { options } = this; const { autolink } = options; if (!autolink) return; return super.url(src, mangle); } // Override smartypants inlineText(src) { const { options, rules } = this; const { quotes, smartypants: isSmarty } = options; // https://github.com/markedjs/marked/blob/b6773fca412c339e0cedd56b63f9fa1583cfd372/src/Tokenizer.js#L643-L658 const cap = rules.inline.text.exec(src); if (cap) { let text; if (this.lexer.state.inRawBlock || this.rules.inline.url.exec(src)) { text = cap[0]; } else { text = escape(isSmarty ? smartypants(cap[0], quotes) : cap[0]); } return { type: 'text', raw: cap[0], text }; } } } module.exports = function(data, options) { const { post_asset_folder, marked: markedCfg, source_dir } = this.config; const { prependRoot, postAsset, dompurify } = markedCfg; const { path, text } = data; // exec filter to extend renderer. const renderer = new Renderer(this); this.execFilterSync('marked:renderer', renderer, {context: this}); const tokenizer = new Tokenizer(); this.execFilterSync('marked:tokenizer', tokenizer, {context: this}); const extensions = []; this.execFilterSync('marked:extensions', extensions, {context: this}); marked.use({extensions}); let postPath = ''; if (path && post_asset_folder && prependRoot && postAsset) { const Post = this.model('Post'); // Windows compatibility, Post.findOne() requires forward slash const source = path.substring(this.source_dir.length).replace(/\\/g, '/'); const post = Post.findOne({ source }); if (post) { const { source: postSource } = post; postPath = join(source_dir, dirname(postSource), basename(postSource, extname(postSource))); } } let sanitizer = function(html) { return html; }; if (dompurify) { if (createDOMPurify === undefined && JSDOM === undefined) { createDOMPurify = require('dompurify'); JSDOM = require('jsdom').JSDOM; } const window = new JSDOM('').window; const DOMPurify = createDOMPurify(window); let param = {}; if (dompurify !== true) { param = dompurify; } sanitizer = function(html) { return DOMPurify.sanitize(html, param); }; } return sanitizer(marked(text, Object.assign({ renderer, tokenizer }, markedCfg, options, { postPath }))); };