2018-03-09 22:59:12 +02:00
|
|
|
const MarkdownIt = require('markdown-it');
|
|
|
|
const Entities = require('html-entities').AllHtmlEntities;
|
2019-07-29 15:43:53 +02:00
|
|
|
const htmlentities = new Entities().encode;
|
2018-03-09 22:59:12 +02:00
|
|
|
const Resource = require('lib/models/Resource.js');
|
|
|
|
const { shim } = require('lib/shim.js');
|
|
|
|
const md5 = require('md5');
|
2018-12-29 19:24:02 +02:00
|
|
|
const StringUtils = require('lib/string-utils.js');
|
2019-07-16 20:05:47 +02:00
|
|
|
const noteStyle = require('./noteStyle');
|
|
|
|
const Setting = require('lib/models/Setting.js');
|
2019-03-08 19:14:17 +02:00
|
|
|
const rules = {
|
|
|
|
image: require('./MdToHtml/rules/image'),
|
|
|
|
checkbox: require('./MdToHtml/rules/checkbox'),
|
|
|
|
katex: require('./MdToHtml/rules/katex'),
|
|
|
|
link_open: require('./MdToHtml/rules/link_open'),
|
2019-05-11 10:49:56 +02:00
|
|
|
html_image: require('./MdToHtml/rules/html_image'),
|
2019-03-08 19:14:17 +02:00
|
|
|
highlight_keywords: require('./MdToHtml/rules/highlight_keywords'),
|
2019-03-13 00:07:02 +02:00
|
|
|
code_inline: require('./MdToHtml/rules/code_inline'),
|
2019-03-08 19:14:17 +02:00
|
|
|
};
|
|
|
|
const setupLinkify = require('./MdToHtml/setupLinkify');
|
|
|
|
const hljs = require('highlight.js');
|
2019-04-02 18:14:48 +02:00
|
|
|
const markdownItAnchor = require('markdown-it-anchor');
|
|
|
|
// The keys must match the corresponding entry in Setting.js
|
|
|
|
const plugins = {
|
2019-07-29 15:43:53 +02:00
|
|
|
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' } },
|
2019-04-02 18:14:48 +02:00
|
|
|
};
|
2017-11-07 20:39:11 +02:00
|
|
|
|
|
|
|
class MdToHtml {
|
2017-11-12 18:33:34 +02:00
|
|
|
constructor(options = null) {
|
|
|
|
if (!options) options = {};
|
|
|
|
|
2017-11-21 21:31:21 +02:00
|
|
|
// Must include last "/"
|
2019-07-29 15:43:53 +02:00
|
|
|
this.resourceBaseUrl_ = 'resourceBaseUrl' in options ? options.resourceBaseUrl : null;
|
2017-11-07 20:39:11 +02:00
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
this.cachedOutputs_ = {};
|
2019-06-21 09:28:59 +02:00
|
|
|
|
|
|
|
this.lastCodeHighlightCacheKey_ = null;
|
|
|
|
this.cachedHighlightedCode_ = {};
|
2018-12-16 19:32:42 +02:00
|
|
|
}
|
|
|
|
|
2017-11-07 20:39:11 +02:00
|
|
|
render(body, style, options = null) {
|
|
|
|
if (!options) options = {};
|
2018-03-09 22:59:12 +02:00
|
|
|
if (!options.postMessageSyntax) options.postMessageSyntax = 'postMessage';
|
|
|
|
if (!options.paddingBottom) options.paddingBottom = '0';
|
2018-12-16 19:32:42 +02:00
|
|
|
if (!options.highlightedKeywords) options.highlightedKeywords = [];
|
2017-11-07 20:39:11 +02:00
|
|
|
|
2019-06-21 09:28:59 +02:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2019-04-21 11:03:10 +02:00
|
|
|
const breaks_ = Setting.value('markdown.softbreaks') ? false : true;
|
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
const cacheKey = md5(escape(body + JSON.stringify(options) + JSON.stringify(style)));
|
|
|
|
const cachedOutput = this.cachedOutputs_[cacheKey];
|
|
|
|
if (cachedOutput) return cachedOutput;
|
2017-11-07 20:39:11 +02:00
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
const context = {
|
|
|
|
css: {},
|
|
|
|
cssFiles: {},
|
|
|
|
assetLoaders: {},
|
|
|
|
};
|
2019-02-28 01:38:50 +02:00
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
const markdownIt = new MarkdownIt({
|
2019-04-21 11:03:10 +02:00
|
|
|
breaks: breaks_,
|
2017-11-13 02:23:12 +02:00
|
|
|
linkify: true,
|
2018-06-22 20:18:15 +02:00
|
|
|
html: true,
|
2019-06-21 09:28:59 +02:00
|
|
|
highlight: (str, lang) => {
|
2019-03-08 19:14:17 +02:00
|
|
|
try {
|
|
|
|
let hlCode = '';
|
2019-06-21 09:28:59 +02:00
|
|
|
|
|
|
|
const cacheKey = md5(str + '_' + lang);
|
|
|
|
|
|
|
|
if (options.codeHighlightCacheKey && this.cachedHighlightedCode_[cacheKey]) {
|
|
|
|
hlCode = this.cachedHighlightedCode_[cacheKey];
|
2019-03-08 19:14:17 +02:00
|
|
|
} else {
|
2019-06-21 09:28:59 +02:00
|
|
|
if (lang && hljs.getLanguage(lang)) {
|
|
|
|
hlCode = hljs.highlight(lang, str, true).value;
|
|
|
|
} else {
|
|
|
|
hlCode = hljs.highlightAuto(str).value;
|
|
|
|
}
|
|
|
|
this.cachedHighlightedCode_[cacheKey] = hlCode;
|
2019-03-08 19:14:17 +02:00
|
|
|
}
|
2018-01-11 21:51:01 +02:00
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
if (shim.isReactNative()) {
|
|
|
|
context.css['hljs'] = shim.loadCssFromJs(options.codeTheme);
|
|
|
|
} else {
|
|
|
|
context.cssFiles['hljs'] = 'highlight/styles/' + options.codeTheme;
|
|
|
|
}
|
2018-12-16 19:32:42 +02:00
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
return '<pre class="hljs"><code>' + hlCode + '</code></pre>';
|
|
|
|
} catch (error) {
|
|
|
|
return '<pre class="hljs"><code>' + markdownIt.utils.escapeHtml(str) + '</code></pre>';
|
|
|
|
}
|
2019-07-29 15:43:53 +02:00
|
|
|
},
|
2018-08-18 11:14:34 +02:00
|
|
|
});
|
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
const ruleOptions = Object.assign({}, options, { resourceBaseUrl: this.resourceBaseUrl_ });
|
2018-08-18 11:14:34 +02:00
|
|
|
|
2019-04-02 18:14:48 +02:00
|
|
|
// To add a plugin, there are three options:
|
2019-03-13 00:07:02 +02:00
|
|
|
//
|
|
|
|
// 1. If the plugin does not need any application specific data, use the standard way:
|
|
|
|
//
|
|
|
|
// const someMarkdownPlugin = require('someMarkdownPlugin');
|
|
|
|
// markdownIt.use(someMarkdownPlugin);
|
|
|
|
//
|
2019-07-29 15:43:53 +02:00
|
|
|
// 2. If the plugin does not need any application specific data, and you want the user
|
2019-04-02 18:14:48 +02:00
|
|
|
// 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
|
2019-03-13 00:07:02 +02:00
|
|
|
// 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.
|
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
markdownIt.use(rules.image(context, ruleOptions));
|
|
|
|
markdownIt.use(rules.checkbox(context, ruleOptions));
|
|
|
|
markdownIt.use(rules.link_open(context, ruleOptions));
|
2019-05-11 10:49:56 +02:00
|
|
|
markdownIt.use(rules.html_image(context, ruleOptions));
|
2019-06-21 09:28:59 +02:00
|
|
|
if (Setting.value('markdown.plugin.katex')) markdownIt.use(rules.katex(context, ruleOptions));
|
2019-03-08 19:14:17 +02:00
|
|
|
markdownIt.use(rules.highlight_keywords(context, ruleOptions));
|
2019-03-13 00:07:02 +02:00
|
|
|
markdownIt.use(rules.code_inline(context, ruleOptions));
|
2019-07-29 15:43:53 +02:00
|
|
|
markdownIt.use(markdownItAnchor);
|
2019-04-02 18:14:48 +02:00
|
|
|
|
|
|
|
for (let key in plugins) {
|
2019-06-21 09:28:59 +02:00
|
|
|
if (Setting.value('markdown.plugin.' + key)) markdownIt.use(plugins[key].module, plugins[key].options);
|
2019-04-02 18:14:48 +02:00
|
|
|
}
|
2018-08-18 11:14:34 +02:00
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
setupLinkify(markdownIt);
|
2018-08-18 11:14:34 +02:00
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
const renderedBody = markdownIt.render(body);
|
2018-02-06 19:58:54 +02:00
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
const cssStrings = noteStyle(style, options);
|
2017-11-07 20:39:11 +02:00
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
for (let k in context.css) {
|
|
|
|
if (!context.css.hasOwnProperty(k)) continue;
|
|
|
|
cssStrings.push(context.css[k]);
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
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);
|
|
|
|
});
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
|
2019-02-18 02:42:52 +02:00
|
|
|
if (options.userCss) cssStrings.push(options.userCss);
|
2019-01-31 10:23:33 +02:00
|
|
|
|
2019-02-18 02:42:52 +02:00
|
|
|
const styleHtml = '<style>' + cssStrings.join('\n') + '</style>';
|
2017-11-07 20:39:11 +02:00
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
const html = styleHtml + '<div id="rendered-md">' + renderedBody + '</div>';
|
2017-11-10 01:28:08 +02:00
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
const output = {
|
|
|
|
html: html,
|
|
|
|
cssFiles: Object.keys(context.cssFiles).map(k => context.cssFiles[k]),
|
|
|
|
};
|
2019-02-06 01:55:09 +02:00
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
// Fow now, we keep only the last entry in the cache
|
|
|
|
this.cachedOutputs_ = {};
|
|
|
|
this.cachedOutputs_[cacheKey] = output;
|
2017-11-07 20:39:11 +02:00
|
|
|
|
2019-03-08 19:14:17 +02:00
|
|
|
return output;
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
|
2019-02-28 01:38:50 +02:00
|
|
|
injectedJavaScript() {
|
2019-03-08 19:14:17 +02:00
|
|
|
return '';
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-01 05:00:28 +02:00
|
|
|
module.exports = MdToHtml;
|