const MarkdownIt = require('markdown-it'); const Entities = require('html-entities').AllHtmlEntities; const htmlentities = new Entities().encode; const Resource = require('lib/models/Resource.js'); const { shim } = require('lib/shim.js'); const md5 = require('md5'); const StringUtils = require('lib/string-utils.js'); const noteStyle = require('./noteStyle'); const Setting = require('lib/models/Setting.js'); const rules = { image: require('./MdToHtml/rules/image'), checkbox: require('./MdToHtml/rules/checkbox'), katex: require('./MdToHtml/rules/katex'), link_open: require('./MdToHtml/rules/link_open'), html_image: require('./MdToHtml/rules/html_image'), highlight_keywords: require('./MdToHtml/rules/highlight_keywords'), code_inline: require('./MdToHtml/rules/code_inline'), }; const setupLinkify = require('./MdToHtml/setupLinkify'); const hljs = require('highlight.js'); const markdownItAnchor = require('markdown-it-anchor'); // The keys must match the corresponding entry in Setting.js const plugins = { mark: { module: require('markdown-it-mark') }, footnote: { module: require('markdown-it-footnote') }, sub: { module: require('markdown-it-sub') }, sup: { module: require('markdown-it-sup') }, deflist: { module: require('markdown-it-deflist') }, abbr: { module: require('markdown-it-abbr') }, emoji: { module: require('markdown-it-emoji') }, insert: { module: require('markdown-it-ins') }, multitable: { module: require('markdown-it-multimd-table'), options: { enableMultilineRows: true, enableRowspan: true } }, toc: { module: require('markdown-it-toc-done-right'), options: { listType: 'ul' } }, }; class MdToHtml { constructor(options = null) { if (!options) options = {}; // Must include last "/" this.resourceBaseUrl_ = 'resourceBaseUrl' in options ? options.resourceBaseUrl : null; this.cachedOutputs_ = {}; this.lastCodeHighlightCacheKey_ = null; this.cachedHighlightedCode_ = {}; } render(body, style, options = null) { if (!options) options = {}; if (!options.postMessageSyntax) options.postMessageSyntax = 'postMessage'; if (!options.paddingBottom) options.paddingBottom = '0'; if (!options.highlightedKeywords) options.highlightedKeywords = []; // The "codeHighlightCacheKey" option indicates what set of cached object should be // associated with this particular Markdown body. It is only used to allow us to // clear the cache whenever switching to a different note. // If "codeHighlightCacheKey" is not specified, code highlighting won't be cached. if (options.codeHighlightCacheKey !== this.lastCodeHighlightCacheKey_ || !options.codeHighlightCacheKey) { this.cachedHighlightedCode_ = {}; this.lastCodeHighlightCacheKey_ = options.codeHighlightCacheKey; } const breaks_ = Setting.value('markdown.softbreaks') ? false : true; const cacheKey = md5(escape(body + JSON.stringify(options) + JSON.stringify(style))); const cachedOutput = this.cachedOutputs_[cacheKey]; if (cachedOutput) return cachedOutput; const context = { css: {}, cssFiles: {}, assetLoaders: {}, }; const markdownIt = new MarkdownIt({ breaks: breaks_, linkify: true, html: true, highlight: (str, lang) => { try { let hlCode = ''; const cacheKey = md5(str + '_' + lang); if (options.codeHighlightCacheKey && this.cachedHighlightedCode_[cacheKey]) { hlCode = this.cachedHighlightedCode_[cacheKey]; } else { if (lang && hljs.getLanguage(lang)) { hlCode = hljs.highlight(lang, str, true).value; } else { hlCode = hljs.highlightAuto(str).value; } this.cachedHighlightedCode_[cacheKey] = hlCode; } if (shim.isReactNative()) { context.css['hljs'] = shim.loadCssFromJs(options.codeTheme); } else { context.cssFiles['hljs'] = 'highlight/styles/' + options.codeTheme; } return '
' + hlCode + '
';
} catch (error) {
return '' + markdownIt.utils.escapeHtml(str) + '
';
}
},
});
const ruleOptions = Object.assign({}, options, { resourceBaseUrl: this.resourceBaseUrl_ });
// To add a plugin, there are three options:
//
// 1. If the plugin does not need any application specific data, use the standard way:
//
// const someMarkdownPlugin = require('someMarkdownPlugin');
// markdownIt.use(someMarkdownPlugin);
//
// 2. If the plugin does not need any application specific data, and you want the user
// to be able to toggle the plugin:
//
// Add the plugin to the plugins object
// const plugins = {
// plugin: require('someMarkdownPlugin'),
// }
//
// And add a corresponding entry into Setting.js
// 'markdown.plugin.mark': {value: true, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable ==mark== syntax')},
//
// 3. If the plugin needs application data (in ruleOptions) or needs to pass data (CSS, files to load, etc.) back
// to the application (using the context object), use the application-specific way:
//
// const imagePlugin = require('./MdToHtml/rules/image');
// markdownIt.use(imagePlugin(context, ruleOptions));
//
// Using the `context` object, a plugin can send back either CSS strings (in .css) or CSS files that need
// to be loaded (in .cssFiles). In general, the desktop app will load the CSS files and the mobile app
// will load the CSS strings.
markdownIt.use(rules.image(context, ruleOptions));
markdownIt.use(rules.checkbox(context, ruleOptions));
markdownIt.use(rules.link_open(context, ruleOptions));
markdownIt.use(rules.html_image(context, ruleOptions));
if (Setting.value('markdown.plugin.katex')) markdownIt.use(rules.katex(context, ruleOptions));
markdownIt.use(rules.highlight_keywords(context, ruleOptions));
markdownIt.use(rules.code_inline(context, ruleOptions));
markdownIt.use(markdownItAnchor);
for (let key in plugins) {
if (Setting.value('markdown.plugin.' + key)) markdownIt.use(plugins[key].module, plugins[key].options);
}
setupLinkify(markdownIt);
const renderedBody = markdownIt.render(body);
const cssStrings = noteStyle(style, options);
for (let k in context.css) {
if (!context.css.hasOwnProperty(k)) continue;
cssStrings.push(context.css[k]);
}
for (let k in context.assetLoaders) {
if (!context.assetLoaders.hasOwnProperty(k)) continue;
context.assetLoaders[k]().catch(error => {
console.warn('MdToHtml: Error loading assets for ' + k + ': ', error.message);
});
}
if (options.userCss) cssStrings.push(options.userCss);
const styleHtml = '';
const html = styleHtml + '