2018-03-09 22:59:12 +02:00
|
|
|
const MarkdownIt = require('markdown-it');
|
|
|
|
const Entities = require('html-entities').AllHtmlEntities;
|
|
|
|
const htmlentities = (new Entities()).encode;
|
|
|
|
const Resource = require('lib/models/Resource.js');
|
|
|
|
const ModelCache = require('lib/ModelCache');
|
2018-03-20 01:04:48 +02:00
|
|
|
const ObjectUtils = require('lib/ObjectUtils');
|
2018-03-09 22:59:12 +02:00
|
|
|
const { shim } = require('lib/shim.js');
|
2018-03-23 19:59:18 +02:00
|
|
|
const { _ } = require('lib/locale');
|
2018-03-09 22:59:12 +02:00
|
|
|
const md5 = require('md5');
|
|
|
|
const MdToHtml_Katex = require('lib/MdToHtml_Katex');
|
2017-11-07 20:39:11 +02:00
|
|
|
|
|
|
|
class MdToHtml {
|
2018-03-09 22:59:12 +02:00
|
|
|
|
2017-11-12 18:33:34 +02:00
|
|
|
constructor(options = null) {
|
|
|
|
if (!options) options = {};
|
|
|
|
|
2017-11-07 20:39:11 +02:00
|
|
|
this.loadedResources_ = {};
|
|
|
|
this.cachedContent_ = null;
|
|
|
|
this.cachedContentKey_ = null;
|
2017-11-28 23:15:22 +02:00
|
|
|
this.modelCache_ = new ModelCache();
|
2017-11-21 21:31:21 +02:00
|
|
|
|
|
|
|
// Must include last "/"
|
2018-03-09 22:59:12 +02:00
|
|
|
this.resourceBaseUrl_ = ('resourceBaseUrl' in options) ? options.resourceBaseUrl : null;
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
makeContentKey(resources, body, style, options) {
|
|
|
|
let k = [];
|
|
|
|
for (let n in resources) {
|
|
|
|
if (!resources.hasOwnProperty(n)) continue;
|
|
|
|
const r = resources[n];
|
|
|
|
k.push(r.id);
|
|
|
|
}
|
2018-01-31 21:01:11 +02:00
|
|
|
k.push(md5(escape(body))); // https://github.com/pvorb/node-md5/issues/41
|
2017-11-07 20:39:11 +02:00
|
|
|
k.push(md5(JSON.stringify(style)));
|
|
|
|
k.push(md5(JSON.stringify(options)));
|
2018-03-09 22:59:12 +02:00
|
|
|
return k.join('_');
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
renderAttrs_(attrs) {
|
2018-03-09 22:59:12 +02:00
|
|
|
if (!attrs) return '';
|
2017-11-07 20:39:11 +02:00
|
|
|
|
|
|
|
let output = [];
|
|
|
|
for (let i = 0; i < attrs.length; i++) {
|
|
|
|
const n = attrs[i][0];
|
|
|
|
const v = attrs[i].length >= 2 ? attrs[i][1] : null;
|
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
if (n === 'alt' && !v) {
|
2017-11-07 20:39:11 +02:00
|
|
|
continue;
|
2018-03-09 22:59:12 +02:00
|
|
|
} else if (n === 'src') {
|
2017-11-07 20:39:11 +02:00
|
|
|
output.push('src="' + htmlentities(v) + '"');
|
|
|
|
} else {
|
2018-03-09 22:59:12 +02:00
|
|
|
output.push(n + '="' + (v ? htmlentities(v) : '') + '"');
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
}
|
2018-03-09 22:59:12 +02:00
|
|
|
return output.join(' ');
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
|
2018-03-23 19:59:18 +02:00
|
|
|
getAttr_(attrs, name, defaultValue = null) {
|
2017-11-07 20:39:11 +02:00
|
|
|
for (let i = 0; i < attrs.length; i++) {
|
|
|
|
if (attrs[i][0] === name) return attrs[i].length > 1 ? attrs[i][1] : null;
|
|
|
|
}
|
2018-03-23 19:59:18 +02:00
|
|
|
return defaultValue;
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
setAttr_(attrs, name, value) {
|
|
|
|
for (let i = 0; i < attrs.length; i++) {
|
|
|
|
if (attrs[i][0] === name) {
|
|
|
|
attrs[i][1] = value;
|
|
|
|
return attrs;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
attrs.push([name, value]);
|
|
|
|
return attrs;
|
|
|
|
}
|
|
|
|
|
|
|
|
renderImage_(attrs, options) {
|
2018-03-09 22:59:12 +02:00
|
|
|
const loadResource = async (id) => {
|
2018-01-21 19:01:37 +02:00
|
|
|
// console.info('Loading resource: ' + id);
|
2017-11-07 20:39:11 +02:00
|
|
|
|
|
|
|
// Initially set to to an empty object to make
|
|
|
|
// it clear that it is being loaded. Otherwise
|
|
|
|
// it sometimes results in multiple calls to
|
|
|
|
// loadResource() for the same resource.
|
|
|
|
this.loadedResources_[id] = {};
|
|
|
|
|
|
|
|
const resource = await Resource.load(id);
|
2017-11-28 23:15:22 +02:00
|
|
|
//const resource = await this.modelCache_.load(Resource, id);
|
2017-11-24 20:59:16 +02:00
|
|
|
|
|
|
|
if (!resource) {
|
|
|
|
// Can happen for example if an image is attached to a note, but the resource hasn't
|
2017-11-28 23:15:22 +02:00
|
|
|
// been downloaded from the sync target yet.
|
2018-03-09 22:59:12 +02:00
|
|
|
console.warn('Cannot load resource: ' + id);
|
2017-11-24 20:59:16 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-11-20 20:25:23 +02:00
|
|
|
this.loadedResources_[id] = resource;
|
2017-11-07 20:39:11 +02:00
|
|
|
|
|
|
|
if (options.onResourceLoaded) options.onResourceLoaded();
|
2018-03-09 22:59:12 +02:00
|
|
|
}
|
2017-11-07 20:39:11 +02:00
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
const title = this.getAttr_(attrs, 'title');
|
|
|
|
const href = this.getAttr_(attrs, 'src');
|
2017-11-07 20:39:11 +02:00
|
|
|
|
|
|
|
if (!Resource.isResourceUrl(href)) {
|
2017-12-07 02:57:36 +02:00
|
|
|
return '<img title="' + htmlentities(title) + '" src="' + href + '"/>';
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const resourceId = Resource.urlToId(href);
|
2017-11-20 20:25:23 +02:00
|
|
|
const resource = this.loadedResources_[resourceId];
|
|
|
|
if (!resource) {
|
2017-11-07 20:39:11 +02:00
|
|
|
loadResource(resourceId);
|
2018-03-09 22:59:12 +02:00
|
|
|
return '';
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
if (!resource.id) return ''; // Resource is being loaded
|
2017-11-07 20:39:11 +02:00
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
const mime = resource.mime ? resource.mime.toLowerCase() : '';
|
|
|
|
if (mime == 'image/png' || mime == 'image/jpg' || mime == 'image/jpeg' || mime == 'image/gif') {
|
|
|
|
let src = './' + Resource.filename(resource);
|
2017-11-21 21:31:21 +02:00
|
|
|
if (this.resourceBaseUrl_ !== null) src = this.resourceBaseUrl_ + src;
|
2018-02-07 22:23:17 +02:00
|
|
|
let output = '<img data-resource-id="' + resource.id + '" title="' + htmlentities(title) + '" src="' + src + '"/>';
|
2017-11-07 20:39:11 +02:00
|
|
|
return output;
|
|
|
|
}
|
2018-03-09 22:59:12 +02:00
|
|
|
|
|
|
|
return '[Image: ' + htmlentities(resource.title) + ' (' + htmlentities(mime) + ')]';
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
renderOpenLink_(attrs, options) {
|
2018-03-09 22:59:12 +02:00
|
|
|
let href = this.getAttr_(attrs, 'href');
|
|
|
|
const text = this.getAttr_(attrs, 'text');
|
2017-11-12 18:33:34 +02:00
|
|
|
const isResourceUrl = Resource.isResourceUrl(href);
|
2018-03-09 22:59:12 +02:00
|
|
|
const title = isResourceUrl ? this.getAttr_(attrs, 'title') : href;
|
2017-11-07 20:39:11 +02:00
|
|
|
|
2018-05-02 16:13:20 +02:00
|
|
|
let resourceIdAttr = "";
|
|
|
|
let icon = "";
|
|
|
|
let hrefAttr = '#';
|
|
|
|
if (isResourceUrl) {
|
|
|
|
const resourceId = Resource.pathToId(href);
|
|
|
|
href = "joplin://" + resourceId;
|
|
|
|
resourceIdAttr = "data-resource-id='" + resourceId + "'";
|
|
|
|
icon = '<span class="resource-icon"></span>';
|
2017-11-07 20:39:11 +02:00
|
|
|
} else {
|
2018-05-02 16:13:20 +02:00
|
|
|
// If the link is a plain URL (as opposed to a resource link), set the href to the actual
|
|
|
|
// link. This allows the link to be exported too when exporting to PDF.
|
|
|
|
hrefAttr = href;
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
2018-05-02 16:13:20 +02:00
|
|
|
|
|
|
|
const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
|
|
|
|
let output = "<a " + resourceIdAttr + " title='" + htmlentities(title) + "' href='" + hrefAttr + "' onclick='" + js + "'>" + icon;
|
|
|
|
return output;
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
renderCloseLink_(attrs, options) {
|
2018-05-02 16:13:20 +02:00
|
|
|
return '</a>';
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
|
2018-02-06 19:58:54 +02:00
|
|
|
rendererPlugin_(language) {
|
2018-02-04 19:12:24 +02:00
|
|
|
if (!language) return null;
|
|
|
|
|
2018-05-10 13:02:39 +02:00
|
|
|
if (!this.rendererPlugins_) {
|
|
|
|
this.rendererPlugins_ = {};
|
|
|
|
this.rendererPlugins_['katex'] = new MdToHtml_Katex();
|
|
|
|
}
|
|
|
|
|
|
|
|
return language in this.rendererPlugins_ ? this.rendererPlugins_[language] : null;
|
2018-02-04 19:12:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
parseInlineCodeLanguage_(content) {
|
|
|
|
const m = content.match(/^\{\.([a-zA-Z0-9]+)\}/);
|
|
|
|
if (m && m.length >= 2) {
|
|
|
|
const language = m[1];
|
|
|
|
return {
|
|
|
|
language: language,
|
|
|
|
newContent: content.substr(language.length + 3),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-03-23 19:59:18 +02:00
|
|
|
urldecode_(str) {
|
|
|
|
try {
|
|
|
|
return decodeURIComponent((str+'').replace(/\+/g, '%20'));
|
|
|
|
} catch (error) {
|
|
|
|
// decodeURIComponent can throw if the string contains non-encoded data (for example "100%")
|
|
|
|
// so in this case just return the non encoded string.
|
|
|
|
return str;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-01-11 21:51:01 +02:00
|
|
|
renderTokens_(markdownIt, tokens, options) {
|
2017-11-07 20:39:11 +02:00
|
|
|
let output = [];
|
2017-11-13 02:23:12 +02:00
|
|
|
let previousToken = null;
|
2017-12-02 01:15:49 +02:00
|
|
|
let anchorAttrs = [];
|
2018-02-04 19:12:24 +02:00
|
|
|
let extraCssBlocks = {};
|
2018-03-23 19:59:18 +02:00
|
|
|
let anchorHrefs = [];
|
2018-02-04 19:12:24 +02:00
|
|
|
|
2017-11-07 20:39:11 +02:00
|
|
|
for (let i = 0; i < tokens.length; i++) {
|
2018-02-04 19:12:24 +02:00
|
|
|
let t = tokens[i];
|
2018-03-09 22:59:12 +02:00
|
|
|
const nextToken = i < tokens.length ? tokens[i+1] : null;
|
2017-11-07 20:39:11 +02:00
|
|
|
|
|
|
|
let tag = t.tag;
|
|
|
|
let openTag = null;
|
|
|
|
let closeTag = null;
|
|
|
|
let attrs = t.attrs ? t.attrs : [];
|
2018-03-13 19:54:40 +02:00
|
|
|
let tokenContent = t.content ? t.content : '';
|
2018-03-09 22:59:12 +02:00
|
|
|
const isCodeBlock = tag === 'code' && t.block;
|
|
|
|
const isInlineCode = t.type === 'code_inline';
|
2018-02-04 19:12:24 +02:00
|
|
|
const codeBlockLanguage = t && t.info ? t.info : null;
|
2018-02-06 19:58:54 +02:00
|
|
|
let rendererPlugin = null;
|
2018-03-09 22:59:12 +02:00
|
|
|
let rendererPluginOptions = { tagType: 'inline' };
|
2018-03-23 19:59:18 +02:00
|
|
|
let linkHref = null;
|
2017-11-07 20:39:11 +02:00
|
|
|
|
2018-02-06 19:58:54 +02:00
|
|
|
if (isCodeBlock) rendererPlugin = this.rendererPlugin_(codeBlockLanguage);
|
2017-11-07 20:39:11 +02:00
|
|
|
|
2018-05-01 17:45:17 +02:00
|
|
|
if (isInlineCode) {
|
2018-02-04 19:12:24 +02:00
|
|
|
openTag = null;
|
2018-06-22 20:18:15 +02:00
|
|
|
} else if (tag && (t.type.indexOf('html_inline') >= 0 || t.type.indexOf('html_block') >= 0)) {
|
2018-02-17 21:28:24 +02:00
|
|
|
openTag = null;
|
2018-03-09 22:59:12 +02:00
|
|
|
} else if (tag && t.type.indexOf('_open') >= 0) {
|
2017-11-07 20:39:11 +02:00
|
|
|
openTag = tag;
|
2018-03-09 22:59:12 +02:00
|
|
|
} else if (tag && t.type.indexOf('_close') >= 0) {
|
2017-11-07 20:39:11 +02:00
|
|
|
closeTag = tag;
|
2018-03-09 22:59:12 +02:00
|
|
|
} else if (tag && t.type.indexOf('inline') >= 0) {
|
2017-11-07 20:39:11 +02:00
|
|
|
openTag = tag;
|
2018-03-09 22:59:12 +02:00
|
|
|
} else if (t.type === 'link_open') {
|
|
|
|
openTag = 'a';
|
2017-11-10 01:28:08 +02:00
|
|
|
} else if (isCodeBlock) {
|
2018-02-06 19:58:54 +02:00
|
|
|
if (rendererPlugin) {
|
2018-02-04 19:12:24 +02:00
|
|
|
openTag = null;
|
|
|
|
} else {
|
2018-03-09 22:59:12 +02:00
|
|
|
openTag = 'pre';
|
2018-02-04 19:12:24 +02:00
|
|
|
}
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (openTag) {
|
2018-03-09 22:59:12 +02:00
|
|
|
if (openTag === 'a') {
|
2017-12-02 01:15:49 +02:00
|
|
|
anchorAttrs.push(attrs);
|
2018-03-23 19:59:18 +02:00
|
|
|
anchorHrefs.push(this.getAttr_(attrs, 'href'));
|
2017-11-07 20:39:11 +02:00
|
|
|
output.push(this.renderOpenLink_(attrs, options));
|
|
|
|
} else {
|
2017-11-10 01:28:08 +02:00
|
|
|
const attrsHtml = this.renderAttrs_(attrs);
|
2018-03-09 22:59:12 +02:00
|
|
|
output.push('<' + openTag + (attrsHtml ? ' ' + attrsHtml : '') + '>');
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-10 01:28:08 +02:00
|
|
|
if (isCodeBlock) {
|
2018-03-09 22:59:12 +02:00
|
|
|
const codeAttrs = ['code'];
|
2018-02-06 19:58:54 +02:00
|
|
|
if (!rendererPlugin) {
|
2018-02-04 19:12:24 +02:00
|
|
|
if (codeBlockLanguage) codeAttrs.push(t.info); // t.info contains the language when the token is a codeblock
|
2018-03-09 22:59:12 +02:00
|
|
|
output.push('<code class="' + codeAttrs.join(' ') + '">');
|
2018-02-04 19:12:24 +02:00
|
|
|
}
|
|
|
|
} else if (isInlineCode) {
|
|
|
|
const result = this.parseInlineCodeLanguage_(tokenContent);
|
|
|
|
if (result) {
|
2018-02-06 19:58:54 +02:00
|
|
|
rendererPlugin = this.rendererPlugin_(result.language);
|
2018-02-04 19:12:24 +02:00
|
|
|
tokenContent = result.newContent;
|
|
|
|
}
|
|
|
|
|
2018-02-06 19:58:54 +02:00
|
|
|
if (!rendererPlugin) {
|
2018-03-09 22:59:12 +02:00
|
|
|
output.push('<code>');
|
2018-02-04 19:12:24 +02:00
|
|
|
}
|
2017-11-10 01:28:08 +02:00
|
|
|
}
|
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
if (t.type === 'math_inline' || t.type === 'math_block') {
|
|
|
|
rendererPlugin = this.rendererPlugin_('katex');
|
|
|
|
rendererPluginOptions = { tagType: t.type === 'math_block' ? 'block' : 'inline' };
|
2018-02-06 19:58:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (rendererPlugin) {
|
2018-03-09 22:59:12 +02:00
|
|
|
rendererPlugin.loadAssets().catch((error) => {
|
|
|
|
console.warn('MdToHtml: Error loading assets for ' + rendererPlugin.name() + ': ', error.message);
|
2018-02-05 19:55:35 +02:00
|
|
|
});
|
|
|
|
}
|
2018-02-04 19:12:24 +02:00
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
if (t.type === 'image') {
|
|
|
|
if (tokenContent) attrs.push(['title', tokenContent]);
|
2017-11-07 20:39:11 +02:00
|
|
|
output.push(this.renderImage_(attrs, options));
|
2018-06-22 20:18:15 +02:00
|
|
|
} else if (t.type === 'html_inline' || t.type === 'html_block') {
|
2018-02-17 21:28:24 +02:00
|
|
|
output.push(t.content);
|
2018-03-09 22:59:12 +02:00
|
|
|
} else if (t.type === 'softbreak') {
|
|
|
|
output.push('<br/>');
|
|
|
|
} else if (t.type === 'hr') {
|
|
|
|
output.push('<hr/>');
|
2017-11-07 20:39:11 +02:00
|
|
|
} else {
|
|
|
|
if (t.children) {
|
2018-01-11 21:51:01 +02:00
|
|
|
const parsedChildren = this.renderTokens_(markdownIt, t.children, options);
|
2017-11-07 20:39:11 +02:00
|
|
|
output = output.concat(parsedChildren);
|
|
|
|
} else {
|
2018-02-04 19:12:24 +02:00
|
|
|
if (tokenContent) {
|
2018-02-06 19:58:54 +02:00
|
|
|
if ((isCodeBlock || isInlineCode) && rendererPlugin) {
|
2018-03-09 22:59:12 +02:00
|
|
|
output = rendererPlugin.processContent(output, tokenContent, isCodeBlock ? 'block' : 'inline');
|
2018-02-06 19:58:54 +02:00
|
|
|
} else if (rendererPlugin) {
|
|
|
|
output = rendererPlugin.processContent(output, tokenContent, rendererPluginOptions.tagType);
|
2018-02-04 19:12:24 +02:00
|
|
|
} else {
|
|
|
|
output.push(htmlentities(tokenContent));
|
|
|
|
}
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-03-09 22:59:12 +02:00
|
|
|
|
|
|
|
if (nextToken && nextToken.tag === 'li' && t.tag === 'p') {
|
|
|
|
closeTag = null;
|
|
|
|
} else if (t.type === 'link_close') {
|
|
|
|
closeTag = 'a';
|
|
|
|
} else if (tag && t.type.indexOf('inline') >= 0) {
|
2017-11-07 20:39:11 +02:00
|
|
|
closeTag = openTag;
|
2017-11-10 01:28:08 +02:00
|
|
|
} else if (isCodeBlock) {
|
2018-02-06 19:58:54 +02:00
|
|
|
if (!rendererPlugin) closeTag = openTag;
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
|
2018-02-04 19:12:24 +02:00
|
|
|
if (isCodeBlock) {
|
2018-02-06 19:58:54 +02:00
|
|
|
if (!rendererPlugin) {
|
2018-03-09 22:59:12 +02:00
|
|
|
output.push('</code>');
|
2018-02-04 19:12:24 +02:00
|
|
|
}
|
|
|
|
} else if (isInlineCode) {
|
2018-02-06 19:58:54 +02:00
|
|
|
if (!rendererPlugin) {
|
2018-03-09 22:59:12 +02:00
|
|
|
output.push('</code>');
|
2018-02-04 19:12:24 +02:00
|
|
|
}
|
|
|
|
}
|
2017-11-10 01:28:08 +02:00
|
|
|
|
2017-11-07 20:39:11 +02:00
|
|
|
if (closeTag) {
|
2018-03-09 22:59:12 +02:00
|
|
|
if (closeTag === 'a') {
|
2018-03-23 19:59:18 +02:00
|
|
|
const currentAnchorAttrs = anchorAttrs.pop();
|
2018-03-24 17:16:58 +02:00
|
|
|
|
|
|
|
// NOTE: Disabled for now due to this:
|
|
|
|
// https://github.com/laurent22/joplin/issues/318#issuecomment-375854848
|
|
|
|
|
|
|
|
// const previousContent = output.length ? output[output.length - 1].trim() : '';
|
|
|
|
// const anchorHref = this.getAttr_(currentAnchorAttrs, 'href', '').trim();
|
2018-03-23 19:59:18 +02:00
|
|
|
|
|
|
|
// Optimisation: If the content of the anchor is the same as the URL, we replace the content
|
|
|
|
// by (Link). This is to shorten the text, which is important especially when the note comes
|
|
|
|
// from imported HTML, which can contain many such links and make the text unreadble. An example
|
|
|
|
// would be a movie review that has multiple links to allow a user to rate the film from 1 to 5 stars.
|
|
|
|
// In the original page, it might be rendered as stars, via CSS, but in the imported note it would look like this:
|
|
|
|
// http://example.com/rate/1 http://example.com/rate/2 http://example.com/rate/3
|
|
|
|
// http://example.com/rate/4 http://example.com/rate/5
|
|
|
|
// which would take a lot of screen space even though it doesn't matter since the user is unlikely
|
|
|
|
// to rate the film from the note. This is actually a nice example, still readable, but there is way
|
|
|
|
// worse that this in notes that come from web-clipped content.
|
|
|
|
// With this change, the links will still be preserved but displayed like
|
|
|
|
// (link) (link) (link) (link) (link)
|
2018-03-24 17:16:58 +02:00
|
|
|
|
|
|
|
// if (this.urldecode_(previousContent) === htmlentities(this.urldecode_(anchorHref))) {
|
|
|
|
// output.pop();
|
|
|
|
// output.push(_('(Link)'));
|
|
|
|
// }
|
2018-03-23 19:59:18 +02:00
|
|
|
|
|
|
|
output.push(this.renderCloseLink_(currentAnchorAttrs, options));
|
2017-11-07 20:39:11 +02:00
|
|
|
} else {
|
2018-03-09 22:59:12 +02:00
|
|
|
output.push('</' + closeTag + '>');
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
2017-11-13 02:23:12 +02:00
|
|
|
}
|
|
|
|
|
2018-02-06 19:58:54 +02:00
|
|
|
if (rendererPlugin) {
|
|
|
|
const extraCss = rendererPlugin.extraCss();
|
|
|
|
const name = rendererPlugin.name();
|
2018-02-04 19:12:24 +02:00
|
|
|
if (extraCss && !(name in extraCssBlocks)) {
|
|
|
|
extraCssBlocks[name] = extraCss;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-13 02:23:12 +02:00
|
|
|
previousToken = t;
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
2018-02-04 19:12:24 +02:00
|
|
|
|
|
|
|
// Insert the extra CSS at the top of the HTML
|
|
|
|
|
2018-03-20 01:04:48 +02:00
|
|
|
if (!ObjectUtils.isEmpty(extraCssBlocks)) {
|
|
|
|
const temp = ['<style>'];
|
|
|
|
for (let n in extraCssBlocks) {
|
|
|
|
if (!extraCssBlocks.hasOwnProperty(n)) continue;
|
|
|
|
temp.push(extraCssBlocks[n]);
|
|
|
|
}
|
|
|
|
temp.push('</style>');
|
2018-02-04 19:12:24 +02:00
|
|
|
|
2018-03-20 01:04:48 +02:00
|
|
|
output = temp.concat(output);
|
|
|
|
}
|
2018-02-04 19:12:24 +02:00
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
return output.join('');
|
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';
|
2017-11-07 20:39:11 +02:00
|
|
|
|
|
|
|
const cacheKey = this.makeContentKey(this.loadedResources_, body, style, options);
|
|
|
|
if (this.cachedContentKey_ === cacheKey) return this.cachedContent_;
|
|
|
|
|
2017-11-13 02:23:12 +02:00
|
|
|
const md = new MarkdownIt({
|
|
|
|
breaks: true,
|
|
|
|
linkify: true,
|
2018-06-22 20:18:15 +02:00
|
|
|
html: true,
|
2017-11-13 02:23:12 +02:00
|
|
|
});
|
2018-01-11 21:51:01 +02:00
|
|
|
|
2018-02-06 19:58:54 +02:00
|
|
|
// This is currently used only so that the $expression$ and $$\nexpression\n$$ blocks are translated
|
|
|
|
// to math_inline and math_block blocks. These blocks are then processed directly with the Katex
|
|
|
|
// library. It is better this way as then it is possible to conditionally load the CSS required by
|
|
|
|
// Katex and use an up-to-date version of Katex (as of 2018, the plugin is still using 0.6, which is
|
|
|
|
// buggy instead of 0.9).
|
2018-03-09 22:59:12 +02:00
|
|
|
md.use(require('markdown-it-katex'));
|
2018-02-06 19:58:54 +02:00
|
|
|
|
2017-11-07 20:39:11 +02:00
|
|
|
// Hack to make checkboxes clickable. Ideally, checkboxes should be parsed properly in
|
|
|
|
// renderTokens_(), but for now this hack works. Marking it with HORRIBLE_HACK so
|
|
|
|
// that it can be removed and replaced later on.
|
|
|
|
const HORRIBLE_HACK = true;
|
|
|
|
|
|
|
|
if (HORRIBLE_HACK) {
|
|
|
|
let counter = -1;
|
2018-04-21 10:25:13 +02:00
|
|
|
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0 || body.indexOf('- [x]') >= 0) {
|
|
|
|
body = body.replace(/- \[(X| |x)\]/, function(v, p1) {
|
2018-03-09 22:59:12 +02:00
|
|
|
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
|
2017-11-07 20:39:11 +02:00
|
|
|
counter++;
|
2018-03-09 22:59:12 +02:00
|
|
|
return '- mJOPmCHECKBOXm' + s + 'm' + counter + 'm';
|
2017-11-07 20:39:11 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-11 21:51:01 +02:00
|
|
|
const env = {};
|
2017-11-07 20:39:11 +02:00
|
|
|
const tokens = md.parse(body, env);
|
|
|
|
|
2018-02-04 19:12:24 +02:00
|
|
|
let renderedBody = this.renderTokens_(md, tokens, options);
|
|
|
|
|
2017-11-07 20:39:11 +02:00
|
|
|
// console.info(body);
|
2017-11-13 19:51:46 +02:00
|
|
|
// console.info(tokens);
|
2018-02-04 19:12:24 +02:00
|
|
|
// console.info(renderedBody);
|
2017-11-07 20:39:11 +02:00
|
|
|
|
|
|
|
if (HORRIBLE_HACK) {
|
|
|
|
let loopCount = 0;
|
2018-03-09 22:59:12 +02:00
|
|
|
while (renderedBody.indexOf('mJOPm') >= 0) {
|
2017-11-07 20:39:11 +02:00
|
|
|
renderedBody = renderedBody.replace(/mJOPmCHECKBOXm([A-Z]+)m(\d+)m/, function(v, type, index) {
|
2018-03-09 22:59:12 +02:00
|
|
|
const js = options.postMessageSyntax + "('checkboxclick:" + type + ':' + index + "'); this.classList.contains('tick') ? this.classList.remove('tick') : this.classList.add('tick'); return false;";
|
|
|
|
return '<a href="#" onclick="' + js + '" class="checkbox ' + (type == 'NOTICK' ? '' : 'tick') + '"><span>' + '' + '</span></a>';
|
2017-11-07 20:39:11 +02:00
|
|
|
});
|
|
|
|
if (loopCount++ >= 9999) break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-09 17:59:33 +02:00
|
|
|
// Support <br> tag to allow newlines inside table cells
|
|
|
|
renderedBody = renderedBody.replace(/<br>/gi, '<br>');
|
|
|
|
|
2017-11-07 20:39:11 +02:00
|
|
|
// https://necolas.github.io/normalize.css/
|
|
|
|
const normalizeCss = `
|
|
|
|
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
|
|
|
article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}
|
|
|
|
pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}
|
|
|
|
b,strong{font-weight:bolder}small{font-size:80%}img{border-style:none}
|
|
|
|
`;
|
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
const fontFamily = 'sans-serif';
|
2018-01-09 22:09:49 +02:00
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
const css = `
|
2017-11-07 20:39:11 +02:00
|
|
|
body {
|
2018-03-09 22:59:12 +02:00
|
|
|
font-size: ` + style.htmlFontSize + `;
|
|
|
|
color: ` + style.htmlColor + `;
|
|
|
|
line-height: ` + style.htmlLineHeight + `;
|
|
|
|
background-color: ` + style.htmlBackgroundColor + `;
|
|
|
|
font-family: ` + fontFamily + `;
|
|
|
|
padding-bottom: ` + options.paddingBottom + `;
|
2017-11-08 19:51:55 +02:00
|
|
|
}
|
2017-12-01 21:04:14 +02:00
|
|
|
p, h1, h2, h3, h4, h5, h6, ul, table {
|
2017-11-10 02:07:37 +02:00
|
|
|
margin-top: 0;
|
2017-11-08 19:51:55 +02:00
|
|
|
margin-bottom: 14px;
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
h1 {
|
2017-12-01 21:04:14 +02:00
|
|
|
font-size: 1.5em;
|
2017-11-07 20:39:11 +02:00
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
h2 {
|
2017-12-01 21:04:14 +02:00
|
|
|
font-size: 1.2em;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
h3, h4, h5, h6 {
|
2017-11-07 20:39:11 +02:00
|
|
|
font-size: 1em;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
a {
|
2018-03-09 22:59:12 +02:00
|
|
|
color: ` + style.htmlLinkColor + `
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
ul {
|
2017-11-13 02:23:12 +02:00
|
|
|
padding-left: 1.3em;
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
2018-05-01 17:45:17 +02:00
|
|
|
li p {
|
|
|
|
margin-bottom: 0;
|
|
|
|
}
|
2018-03-09 22:46:28 +02:00
|
|
|
.resource-icon {
|
|
|
|
display: inline-block;
|
|
|
|
position: relative;
|
|
|
|
top: .5em;
|
|
|
|
text-decoration: none;
|
2018-03-12 10:30:10 +02:00
|
|
|
width: 1.15em;
|
2018-03-09 22:46:28 +02:00
|
|
|
height: 1.5em;
|
|
|
|
margin-right: 0.4em;
|
|
|
|
background-color: ` + style.htmlColor + `;
|
|
|
|
/* Awesome Font file */
|
|
|
|
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'><path d='M369.9 97.9L286 14C277 5 264.8-.1 252.1-.1H48C21.5 0 0 21.5 0 48v416c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V131.9c0-12.7-5.1-25-14.1-34zM332.1 128H256V51.9l76.1 76.1zM48 464V48h160v104c0 13.3 10.7 24 24 24h104v288H48z'/></svg>");
|
|
|
|
}
|
2017-11-07 20:39:11 +02:00
|
|
|
a.checkbox {
|
2017-11-20 01:34:41 +02:00
|
|
|
display: inline-block;
|
2017-11-07 20:39:11 +02:00
|
|
|
position: relative;
|
2017-11-20 01:34:41 +02:00
|
|
|
top: .5em;
|
2017-11-07 20:39:11 +02:00
|
|
|
text-decoration: none;
|
2017-11-21 21:05:59 +02:00
|
|
|
width: 1.65em; /* Need to cut a bit the right border otherwise the SVG will display a black line */
|
|
|
|
height: 1.7em;
|
2017-11-20 01:34:41 +02:00
|
|
|
margin-right: .3em;
|
2018-03-09 22:59:12 +02:00
|
|
|
background-color: ` + style.htmlColor + `;
|
2017-11-20 01:34:41 +02:00
|
|
|
/* Awesome Font square-o */
|
|
|
|
-webkit-mask: url("data:image/svg+xml;utf8,<svg viewBox='0 0 1792 1792' xmlns='http://www.w3.org/2000/svg'><path d='M1312 256h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-832q0-66-47-113t-113-47zm288 160v832q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q119 0 203.5 84.5t84.5 203.5z'/></svg>");
|
|
|
|
}
|
|
|
|
a.checkbox.tick {
|
2017-11-21 21:05:59 +02:00
|
|
|
left: .1245em; /* square-o and check-square-o aren't exactly aligned so add this extra gap to align them */
|
2017-11-20 01:34:41 +02:00
|
|
|
/* Awesome Font check-square-o */
|
|
|
|
-webkit-mask: url("data:image/svg+xml;utf8,<svg viewBox='0 0 1792 1792' xmlns='http://www.w3.org/2000/svg'><path d='M1472 930v318q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q63 0 117 25 15 7 18 23 3 17-9 29l-49 49q-10 10-23 10-3 0-9-2-23-6-45-6h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-254q0-13 9-22l64-64q10-10 23-10 6 0 12 3 20 8 20 29zm231-489l-814 814q-24 24-57 24t-57-24l-430-430q-24-24-24-57t24-57l110-110q24-24 57-24t57 24l263 263 647-647q24-24 57-24t57 24l110 110q24 24 24 57t-24 57z'/></svg>");
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
table {
|
|
|
|
border-collapse: collapse;
|
|
|
|
}
|
|
|
|
td, th {
|
|
|
|
border: 1px solid silver;
|
|
|
|
padding: .5em 1em .5em 1em;
|
2018-03-09 22:59:12 +02:00
|
|
|
font-size: ` + style.htmlFontSize + `;
|
|
|
|
color: ` + style.htmlColor + `;
|
|
|
|
background-color: ` + style.htmlBackgroundColor + `;
|
|
|
|
font-family: ` + fontFamily + `;
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
hr {
|
2017-11-13 02:23:12 +02:00
|
|
|
border: none;
|
2018-03-09 22:59:12 +02:00
|
|
|
border-bottom: 1px solid ` + style.htmlDividerColor + `;
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
img {
|
2017-11-12 19:58:28 +02:00
|
|
|
width: auto;
|
|
|
|
max-width: 100%;
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
2018-01-11 21:51:01 +02:00
|
|
|
|
2018-03-12 20:01:47 +02:00
|
|
|
@media print {
|
|
|
|
body {
|
|
|
|
height: auto !important;
|
|
|
|
}
|
|
|
|
|
|
|
|
a.checkbox {
|
|
|
|
border: 1pt solid ` + style.htmlColor + `;
|
|
|
|
border-radius: 2pt;
|
|
|
|
width: 1em;
|
|
|
|
height: 1em;
|
|
|
|
line-height: 1em;
|
|
|
|
text-align: center;
|
|
|
|
top: .4em;
|
|
|
|
}
|
|
|
|
|
|
|
|
a.checkbox.tick:after {
|
|
|
|
content: "X";
|
|
|
|
}
|
|
|
|
|
|
|
|
a.checkbox.tick {
|
|
|
|
top: 0;
|
|
|
|
left: -0.02em;
|
|
|
|
color: ` + style.htmlColor + `;
|
|
|
|
}
|
2018-01-11 21:51:01 +02:00
|
|
|
}
|
2017-11-07 20:39:11 +02:00
|
|
|
`;
|
|
|
|
|
2018-03-12 10:30:10 +02:00
|
|
|
const styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>';
|
2017-11-07 20:39:11 +02:00
|
|
|
|
|
|
|
const output = styleHtml + renderedBody;
|
2017-11-10 01:28:08 +02:00
|
|
|
|
2017-11-07 20:39:11 +02:00
|
|
|
this.cachedContent_ = output;
|
|
|
|
this.cachedContentKey_ = cacheKey;
|
|
|
|
return this.cachedContent_;
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleTickAt(body, index) {
|
|
|
|
let counter = -1;
|
2018-04-21 10:25:13 +02:00
|
|
|
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0 || body.indexOf('- [x]') >= 0) {
|
2017-11-07 20:39:11 +02:00
|
|
|
counter++;
|
|
|
|
|
2018-04-21 10:25:13 +02:00
|
|
|
body = body.replace(/- \[(X| |x)\]/, function(v, p1) {
|
2018-03-09 22:59:12 +02:00
|
|
|
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
|
2017-11-07 20:39:11 +02:00
|
|
|
if (index == counter) {
|
2018-03-09 22:59:12 +02:00
|
|
|
s = s == 'NOTICK' ? 'TICK' : 'NOTICK';
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
2018-03-09 22:59:12 +02:00
|
|
|
return '°°JOP°CHECKBOX°' + s + '°°';
|
2017-11-07 20:39:11 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
body = body.replace(/°°JOP°CHECKBOX°NOTICK°°/g, '- [ ]');
|
|
|
|
body = body.replace(/°°JOP°CHECKBOX°TICK°°/g, '- [X]');
|
2017-11-07 20:39:11 +02:00
|
|
|
|
|
|
|
return body;
|
|
|
|
}
|
|
|
|
|
|
|
|
handleCheckboxClick(msg, noteBody) {
|
2018-03-09 22:59:12 +02:00
|
|
|
msg = msg.split(':');
|
2017-11-07 20:39:11 +02:00
|
|
|
let index = Number(msg[msg.length - 1]);
|
|
|
|
let currentState = msg[msg.length - 2]; // Not really needed but keep it anyway
|
2018-03-09 22:59:12 +02:00
|
|
|
return this.toggleTickAt(noteBody, index);
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
2018-03-09 22:59:12 +02:00
|
|
|
|
2017-11-07 20:39:11 +02:00
|
|
|
}
|
|
|
|
|
2018-03-09 22:59:12 +02:00
|
|
|
module.exports = MdToHtml;
|