1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-11 18:24:43 +02:00

Finished converting plugins

This commit is contained in:
Laurent Cozic 2020-10-20 19:56:34 +01:00
parent fe90d92e01
commit 98bf3bde8d
19 changed files with 311 additions and 329 deletions

View File

@ -224,7 +224,14 @@ ReactNativeClient/lib/InMemoryCache.js
ReactNativeClient/lib/joplin-renderer/MarkupToHtml.js
ReactNativeClient/lib/joplin-renderer/MdToHtml.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/code_inline.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fountain.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/highlight_keywords.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/html_image.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/image.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/katex.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/link_open.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/joplin-renderer/noteStyle.js

7
.gitignore vendored
View File

@ -218,7 +218,14 @@ ReactNativeClient/lib/InMemoryCache.js
ReactNativeClient/lib/joplin-renderer/MarkupToHtml.js
ReactNativeClient/lib/joplin-renderer/MdToHtml.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/code_inline.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fountain.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/highlight_keywords.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/html_image.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/image.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/katex.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/link_open.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/joplin-renderer/noteStyle.js

View File

@ -167,7 +167,14 @@ ReactNativeClient/lib/InMemoryCache.js
ReactNativeClient/lib/joplin-renderer/MarkupToHtml.js
ReactNativeClient/lib/joplin-renderer/MdToHtml.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/code_inline.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fountain.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/highlight_keywords.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/html_image.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/image.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/katex.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/link_open.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/joplin-renderer/noteStyle.js

View File

@ -1,6 +1,6 @@
import { PluginStates } from 'lib/services/plugins/reducer';
import contentScriptsToRendererRules from 'lib/services/plugins/utils/contentScriptsToRendererRules';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { ResourceInfos } from './types';
import markupLanguageUtils from 'lib/markupLanguageUtils';
import Setting from 'lib/models/Setting';
@ -22,6 +22,13 @@ interface MarkupToHtmlOptions {
export default function useMarkupToHtml(deps:HookDependencies) {
const { themeId, customCss, plugins } = deps;
const markupToHtml = useMemo(() => {
return markupLanguageUtils.newMarkupToHtml({
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
extraRendererRules: contentScriptsToRendererRules(plugins),
});
}, [plugins]);
return useCallback(async (markupLanguage: number, md: string, options: MarkupToHtmlOptions = null): Promise<any> => {
options = {
replaceResourceInternalToExternalLinks: false,
@ -42,11 +49,6 @@ export default function useMarkupToHtml(deps:HookDependencies) {
delete options.replaceResourceInternalToExternalLinks;
const markupToHtml = markupLanguageUtils.newMarkupToHtml({
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
extraRendererRules: contentScriptsToRendererRules(plugins),
});
const result = await markupToHtml.render(markupLanguage, md, theme, Object.assign({}, {
codeTheme: theme.codeThemeCss,
userCss: customCss || '',
@ -57,5 +59,5 @@ export default function useMarkupToHtml(deps:HookDependencies) {
}, options));
return result;
}, [themeId, customCss, plugins]);
}, [themeId, customCss, markupToHtml]);
}

View File

@ -42,10 +42,6 @@ export default class MarkupToHtml {
return this.renderers_[markupLanguage];
}
injectedJavaScript() {
return '';
}
stripMarkup(markupLanguage:MarkupLanguage, markup:string, options:any = null) {
if (!markup) return '';

View File

@ -8,7 +8,6 @@ const md5 = require('md5');
interface RendererRule {
install(context:any, ruleOptions:any):any,
assets?(theme:any):any,
rule?: any, // TODO: remove
plugin?: any,
}
@ -29,18 +28,17 @@ interface RendererPlugins {
const rules:RendererRules = {
fence: require('./MdToHtml/rules/fence').default,
sanitize_html: require('./MdToHtml/rules/sanitize_html').default,
image: require('./MdToHtml/rules/image'),
image: require('./MdToHtml/rules/image').default,
checkbox: require('./MdToHtml/rules/checkbox').default,
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'),
fountain: require('./MdToHtml/rules/fountain'),
katex: require('./MdToHtml/rules/katex').default,
link_open: require('./MdToHtml/rules/link_open').default,
html_image: require('./MdToHtml/rules/html_image').default,
highlight_keywords: require('./MdToHtml/rules/highlight_keywords').default,
code_inline: require('./MdToHtml/rules/code_inline').default,
fountain: require('./MdToHtml/rules/fountain').default,
mermaid: require('./MdToHtml/rules/mermaid').default,
};
// const eventManager = require('lib/eventManager').default;
const setupLinkify = require('./MdToHtml/setupLinkify');
const hljs = require('highlight.js');
const uslug = require('uslug');
@ -101,6 +99,7 @@ interface PluginContext {
css: any
pluginAssets: any,
cache: any,
userData: any,
}
interface RenderResultPluginAsset {
@ -119,9 +118,33 @@ export interface RuleOptions {
context: PluginContext,
theme: any,
postMessageSyntax: string,
ResourceModel: any,
resourceBaseUrl: string,
resources: any, // resourceId: Resource
// Used by checkboxes to specify how it should be rendered
checkboxRenderingType?: number,
// Used by the keyword highlighting plugin (mobile only)
highlightedKeywords?: any[],
// Use by resource-rendering logic to signify that it should be rendered
// as a plain HTML string without any attached JavaScript. Used for example
// when exporting to HTML.
plainResourceRendering?: boolean,
// Use in mobile app to enable long-pressing an image or a linkg
// to display a context menu. Used in `image.ts` and `link_open.ts`
enableLongPress?: boolean,
// Used in mobile app when enableLongPress = true. Tells for how long
// the resource should be pressed before the menu is shown.
longPressDelay?: number,
// Use by `link_open` rule.
// linkRenderingType = 1 is the regular rendering and clicking on it is handled via embedded JS (in onclick attribute)
// linkRenderingType = 2 gives a plain link with no JS. Caller needs to handle clicking on the link.
linkRenderingType?: number,
}
export default class MdToHtml {
@ -129,7 +152,6 @@ export default class MdToHtml {
private resourceBaseUrl_:string;
private ResourceModel_:any;
private contextCache_:any;
private tempDir_:string;
private fsDriver_:any;
private cachedOutputs_:any = {};
@ -139,8 +161,9 @@ export default class MdToHtml {
// Markdown-It plugin options (not Joplin plugin options)
private pluginOptions_:any = {};
private extraRendererRules_:RendererRules = {};
private allProcessedAssets_:any = {};
constructor(options:Options = null) {
public constructor(options:Options = null) {
if (!options) options = {};
// Must include last "/"
@ -150,7 +173,6 @@ export default class MdToHtml {
this.pluginOptions_ = options.pluginOptions ? options.pluginOptions : {};
this.contextCache_ = inMemoryCache;
this.tempDir_ = options.tempDir;
this.fsDriver_ = {
writeFile: (/* path, content, encoding = 'base64'*/) => { throw new Error('writeFile not set'); },
exists: (/* path*/) => { throw new Error('exists not set'); },
@ -170,22 +192,18 @@ export default class MdToHtml {
}
}
fsDriver() {
private fsDriver() {
return this.fsDriver_;
}
tempDir() {
return this.tempDir_;
}
static pluginNames() {
public static pluginNames() {
const output = [];
for (const n in rules) output.push(n);
for (const n in plugins) output.push(n);
return output;
}
pluginOptions(name:string) {
private pluginOptions(name:string) {
let o = this.pluginOptions_[name] ? this.pluginOptions_[name] : {};
o = Object.assign({
enabled: true,
@ -193,7 +211,7 @@ export default class MdToHtml {
return o;
}
pluginEnabled(name:string) {
private pluginEnabled(name:string) {
return this.pluginOptions(name).enabled;
}
@ -203,7 +221,7 @@ export default class MdToHtml {
this.extraRendererRules_[id] = module;
}
processPluginAssets(pluginAssets:PluginAssets):RenderResult {
private processPluginAssets(pluginAssets:PluginAssets):RenderResult {
const files:RenderResultPluginAsset[] = [];
const cssStrings = [];
for (const pluginName in pluginAssets) {
@ -246,7 +264,13 @@ export default class MdToHtml {
};
}
private allUnprocessedAssets(theme:any) {
// This return all the assets for all the plugins. Since it is called
// on each render, the result is cached.
private allProcessedAssets(theme:any, codeTheme:string) {
const cacheKey:string = theme.cacheKey + codeTheme;
if (this.allProcessedAssets_[cacheKey]) return this.allProcessedAssets_[cacheKey];
const assets:any = {};
for (const key in rules) {
if (!this.pluginEnabled(key)) continue;
@ -257,11 +281,19 @@ export default class MdToHtml {
}
}
return assets;
assets['highlight.js'] = [{ name: codeTheme }];
const output = this.processPluginAssets(assets);
this.allProcessedAssets_ = {
[cacheKey]: output,
};
return output;
}
// TODO: remove
async allAssets(theme:any) {
// This is similar to allProcessedAssets() but used only by the Rich Text editor
public async allAssets(theme:any) {
const assets:any = {};
for (const key in rules) {
if (!this.pluginEnabled(key)) continue;
@ -278,7 +310,7 @@ export default class MdToHtml {
return output.pluginAssets;
}
async outputAssetsToExternalAssets_(output:any) {
private async outputAssetsToExternalAssets_(output:any) {
for (const cssString of output.cssStrings) {
output.pluginAssets.push(await this.fsDriver().cacheCssToFile(cssString));
}
@ -286,7 +318,7 @@ export default class MdToHtml {
return output;
}
removeMarkdownItWrappingParagraph_(html:string) {
private removeMarkdownItWrappingParagraph_(html:string) {
// <p></p>\n
if (html.length < 8) return html;
if (html.substr(0, 3) !== '<p>') return html;
@ -294,7 +326,7 @@ export default class MdToHtml {
return html.substring(3, html.length - 5);
}
clearCache() {
public clearCache() {
this.cachedOutputs_ = {};
}
@ -338,7 +370,7 @@ export default class MdToHtml {
css: {},
pluginAssets: {},
cache: this.contextCache_,
// options: ruleOptions,
userData: {},
};
const markdownIt = new MarkdownIt({
@ -370,10 +402,6 @@ export default class MdToHtml {
this.cachedHighlightedCode_[cacheKey] = hlCode;
}
context.pluginAssets['highlight.js'] = [
{ name: options.codeTheme },
];
outputCodeHtml = hlCode;
} catch (error) {
outputCodeHtml = markdownIt.utils.escapeHtml(trimmedStr);
@ -417,19 +445,14 @@ export default class MdToHtml {
for (const key in allRules) {
if (!this.pluginEnabled(key)) continue;
const rule = allRules[key];
if (rule.plugin) {
const pluginOptions = {
context: context,
...ruleOptions,
...(ruleOptions.plugins[key] ? ruleOptions.plugins[key] : {}),
};
markdownIt.use(rule.plugin, pluginOptions);
} else {
const ruleInstall:Function = rule.install ? rule.install : (rule as any);
markdownIt.use(ruleInstall(context, { ...ruleOptions }));
}
const rule = allRules[key];
markdownIt.use(rule.plugin, {
context: context,
...ruleOptions,
...(ruleOptions.plugins[key] ? ruleOptions.plugins[key] : {}),
});
}
markdownIt.use(markdownItAnchor, { slugify: slugify });
@ -440,20 +463,13 @@ export default class MdToHtml {
}
}
// const extraPlugins = eventManager.filterEmit('mdToHtmlPlugins', {});
// for (const key in extraPlugins) {
// markdownIt.use(extraPlugins[key].module, extraPlugins[key].options);
// }
setupLinkify(markdownIt);
const renderedBody = markdownIt.render(body, context);
const pluginAssets = this.allUnprocessedAssets(theme);
let cssStrings = noteStyle(options.theme);
let output = this.processPluginAssets(pluginAssets); // context.pluginAssets);
let output = { ...this.allProcessedAssets(theme, options.codeTheme) };
cssStrings = cssStrings.concat(output.cssStrings);
if (options.userCss) cssStrings.push(options.userCss);
@ -485,7 +501,4 @@ export default class MdToHtml {
return output;
}
injectedJavaScript() {
return '';
}
}

View File

@ -1,11 +1,11 @@
function installRule(markdownIt) {
function plugin(markdownIt:any) {
const defaultRender =
markdownIt.renderer.rules.code_inline ||
function(tokens, idx, options, env, self) {
function(tokens:any, idx:any, options:any, _env:any, self:any) {
return self.renderToken(tokens, idx, options);
};
markdownIt.renderer.rules.code_inline = (tokens, idx, options, env, self) => {
markdownIt.renderer.rules.code_inline = (tokens:any[], idx:number, options:any, env:any, self:any) => {
const token = tokens[idx];
let tokenClass = token.attrGet('class');
if (!tokenClass) tokenClass = '';
@ -15,8 +15,6 @@ function installRule(markdownIt) {
};
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
export default {
plugin,
};

View File

@ -8,7 +8,7 @@
// So we modify the code below to allow highlight() to return an object that tells how to render
// the code.
function installRule(markdownIt:any) {
function plugin(markdownIt:any) {
// @ts-ignore: Keep the function signature as-is despite unusued arguments
markdownIt.renderer.rules.fence = function(tokens:any[], idx:number, options:any, env:any, slf:any) {
let token = tokens[idx],
@ -63,8 +63,7 @@ function installRule(markdownIt:any) {
};
}
export default function() {
return function(md:any) {
installRule(md);
};
}
export default {
plugin,
};

View File

@ -1,6 +1,6 @@
const fountain = require('../../vendor/fountain.min.js');
const fountainCss = function() {
const pluginAssets = function() {
return [
{
inline: true,
@ -102,7 +102,7 @@ const fountainCss = function() {
];
};
function renderFountainScript(markdownIt, content) {
function renderFountainScript(markdownIt:any, content:string) {
const result = fountain.parse(content);
return `
@ -118,30 +118,19 @@ function renderFountainScript(markdownIt, content) {
`;
}
function addContextAssets(context) {
if ('fountain' in context.pluginAssets) return;
context.pluginAssets['fountain'] = fountainCss();
}
function installRule(markdownIt, mdOptions, ruleOptions, context) {
const defaultRender = markdownIt.renderer.rules.fence || function(tokens, idx, options, env, self) {
function plugin(markdownIt:any) {
const defaultRender = markdownIt.renderer.rules.fence || function(tokens:any[], idx:number, options:any, _env:any, self:any) {
return self.renderToken(tokens, idx, options);
};
markdownIt.renderer.rules.fence = function(tokens, idx, options, env, self) {
markdownIt.renderer.rules.fence = function(tokens:any[], idx:number, options:any, env:any, self:any) {
const token = tokens[idx];
if (token.info !== 'fountain') return defaultRender(tokens, idx, options, env, self);
addContextAssets(context);
return renderFountainScript(markdownIt, token.content);
};
}
module.exports = {
install: function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions, context);
};
},
assets: fountainCss,
export default {
plugin,
assets: pluginAssets,
};

View File

@ -1,7 +1,11 @@
// This plugin is used only on mobile, to highlight search results.
import { RuleOptions } from "lib/joplin-renderer/MdToHtml";
const stringUtils = require('../../stringUtils.js');
const md5 = require('md5');
function createHighlightedTokens(Token, splitted) {
function createHighlightedTokens(Token:any, splitted:string[]) {
let token;
const output = [];
@ -30,10 +34,11 @@ function createHighlightedTokens(Token, splitted) {
return output;
}
function installRule(markdownIt, mdOptions, ruleOptions) {
// function installRule(markdownIt, mdOptions, ruleOptions) {
function plugin(markdownIt:any, ruleOptions:RuleOptions) {
const divider = md5(Date.now().toString() + Math.random().toString());
markdownIt.core.ruler.push('highlight_keywords', state => {
markdownIt.core.ruler.push('highlight_keywords', (state:any) => {
const keywords = ruleOptions.highlightedKeywords;
if (!keywords || !keywords.length) return;
@ -60,8 +65,6 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
});
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};
export default {
plugin,
}

View File

@ -1,39 +1,40 @@
// const Resource = require('lib/models/Resource.js');
import { RuleOptions } from "lib/joplin-renderer/MdToHtml";
const htmlUtils = require('../../htmlUtils.js');
const utils = require('../../utils');
function renderImageHtml(before, src, after, ruleOptions) {
function renderImageHtml(before:string, src:string, after:string, ruleOptions:RuleOptions) {
const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl);
if (typeof r === 'string') return r;
if (r) return `<img ${before} ${htmlUtils.attributesHtml(r)} ${after}/>`;
return `[Image: ${src}]`;
}
function installRule(markdownIt, mdOptions, ruleOptions) {
function plugin(markdownIt:any, ruleOptions:RuleOptions) {
const Resource = ruleOptions.ResourceModel;
const htmlBlockDefaultRender =
markdownIt.renderer.rules.html_block ||
function(tokens, idx, options, env, self) {
function(tokens:any[], idx:number, options:any, _env:any, self:any) {
return self.renderToken(tokens, idx, options);
};
const htmlInlineDefaultRender =
markdownIt.renderer.rules.html_inline ||
function(tokens, idx, options, env, self) {
function(tokens:any[], idx:number, options:any, _env:any, self:any) {
return self.renderToken(tokens, idx, options);
};
const imageRegex = /<img(.*?)src=["'](.*?)["'](.*?)>/gi;
const handleImageTags = function(defaultRender) {
return function(tokens, idx, options, env, self) {
const handleImageTags = function(defaultRender:Function) {
return function(tokens:any[], idx:number, options:any, env:any, self:any) {
const token = tokens[idx];
const content = token.content;
if (!content.match(imageRegex)) return defaultRender(tokens, idx, options, env, self);
return content.replace(imageRegex, (v, before, src, after) => {
return content.replace(imageRegex, (_v:any, before:string, src:string, after:string) => {
if (!Resource.isResourceUrl(src)) return `<img${before}src="${src}"${after}>`;
return renderImageHtml(before, src, after, ruleOptions);
});
@ -46,8 +47,4 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
markdownIt.renderer.rules.html_inline = handleImageTags(htmlInlineDefaultRender);
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};
export default { plugin }

View File

@ -1,11 +1,13 @@
import { RuleOptions } from "lib/joplin-renderer/MdToHtml";
// const Resource = require('lib/models/Resource.js');
const utils = require('../../utils');
const htmlUtils = require('../../htmlUtils.js');
function installRule(markdownIt, mdOptions, ruleOptions) {
function plugin(markdownIt:any, ruleOptions:RuleOptions) {
const defaultRender = markdownIt.renderer.rules.image;
markdownIt.renderer.rules.image = (tokens, idx, options, env, self) => {
markdownIt.renderer.rules.image = (tokens:any[], idx:number, options:any, env:any, self:any) => {
const Resource = ruleOptions.ResourceModel;
const token = tokens[idx];
@ -19,12 +21,11 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
if (r) {
let js = '';
if (ruleOptions.enableLongPress) {
const longPressDelay = ruleOptions.longPressDelay ? ruleOptions.longPressDelay : 500;
const id = r['data-resource-id'];
const longPressHandler = `${ruleOptions.postMessageSyntax}('longclick:${id}')`;
const touchStart = `t=setTimeout(()=>{t=null; ${longPressHandler};}, ${longPressDelay});`;
const touchStart = `t=setTimeout(()=>{t=null; ${longPressHandler};}, ${ruleOptions.longPressDelay});`;
const touchEnd = 'if (!!t) clearTimeout(t); t=null';
js = ` ontouchstart="${touchStart}" ontouchend="${touchEnd}"`;
@ -36,8 +37,4 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
};
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};
export default { plugin };

View File

@ -1,8 +1,4 @@
/* eslint prefer-const: 0*/
// Based on https://github.com/waylonflinn/markdown-it-katex
'use strict';
import { RuleOptions } from "lib/joplin-renderer/MdToHtml";
let katex = require('katex');
const md5 = require('md5');
@ -46,7 +42,7 @@ function katexStyle() {
// Test if potential opening or closing delimieter
// Assumes that there is a "$" at state.src[pos]
function isValidDelim(state, pos) {
function isValidDelim(state:any, pos:number) {
let prevChar,
nextChar,
max = state.posMax,
@ -71,7 +67,7 @@ function isValidDelim(state, pos) {
};
}
function math_inline(state, silent) {
function math_inline(state:any, silent:boolean) {
let start, match, token, res, pos;
if (state.src[state.pos] !== '$') {
@ -146,7 +142,7 @@ function math_inline(state, silent) {
return true;
}
function math_block(state, start, end, silent) {
function math_block(state:any, start:number, end:number, silent:boolean) {
let firstLine,
lastLine,
next,
@ -212,80 +208,71 @@ function math_block(state, start, end, silent) {
return true;
}
const cache_ = {};
const cache_:any = {};
module.exports = {
install: function(context) {
function renderToStringWithCache(latex:string, katexOptions:any) {
const cacheKey = md5(escape(latex) + escape(stringifySafe(katexOptions)));
if (cacheKey in cache_) {
return cache_[cacheKey];
} else {
const beforeMacros = stringifySafe(katexOptions.macros);
const output = katex.renderToString(latex, katexOptions);
const afterMacros = stringifySafe(katexOptions.macros);
// Don't cache the formulas that add macros, otherwise
// they won't be added on second run.
if (beforeMacros === afterMacros) cache_[cacheKey] = output;
return output;
}
}
export default {
plugin: function(markdownIt:any, options:RuleOptions) {
// Keep macros that persist across Katex blocks to allow defining a macro
// in one block and re-using it later in other blocks.
// https://github.com/laurent22/joplin/issues/1105
context.__katex = { macros: {} };
if (!options.context.userData.__katex) options.context.userData.__katex = { macros: {} };
const addContextAssets = () => {
context.pluginAssets['katex'] = katexStyle();
};
const katexOptions:any = {}
katexOptions.macros = options.context.userData.__katex.macros;
katexOptions.trust = true;
function renderToStringWithCache(latex, options) {
const cacheKey = md5(escape(latex) + escape(stringifySafe(options)));
if (cacheKey in cache_) {
return cache_[cacheKey];
} else {
const beforeMacros = stringifySafe(options.macros);
const output = katex.renderToString(latex, options);
const afterMacros = stringifySafe(options.macros);
// Don't cache the formulas that add macros, otherwise
// they won't be added on second run.
if (beforeMacros === afterMacros) cache_[cacheKey] = output;
return output;
// set KaTeX as the renderer for markdown-it-simplemath
const katexInline = function(latex:string) {
katexOptions.displayMode = false;
try {
return `<span class="joplin-editable"><span class="joplin-source" data-joplin-language="katex" data-joplin-source-open="$" data-joplin-source-close="$">${markdownIt.utils.escapeHtml(latex)}</span>${renderToStringWithCache(latex, katexOptions)}</span>`;
} catch (error) {
console.error('Katex error for:', latex, error);
return latex;
}
}
return function(md, options) {
// Default options
options = options || {};
options.macros = context.__katex.macros;
options.trust = true;
// set KaTeX as the renderer for markdown-it-simplemath
const katexInline = function(latex) {
options.displayMode = false;
try {
return `<span class="joplin-editable"><span class="joplin-source" data-joplin-language="katex" data-joplin-source-open="$" data-joplin-source-close="$">${md.utils.escapeHtml(latex)}</span>${renderToStringWithCache(latex, options)}</span>`;
} catch (error) {
console.error('Katex error for:', latex, error);
return latex;
}
};
const inlineRenderer = function(tokens, idx) {
addContextAssets();
return katexInline(tokens[idx].content);
};
const katexBlock = function(latex) {
options.displayMode = true;
try {
return `<div class="joplin-editable"><pre class="joplin-source" data-joplin-language="katex" data-joplin-source-open="$$&#10;" data-joplin-source-close="&#10;$$&#10;">${md.utils.escapeHtml(latex)}</pre>${renderToStringWithCache(latex, options)}</div>`;
} catch (error) {
console.error('Katex error for:', latex, error);
return latex;
}
};
const blockRenderer = function(tokens, idx) {
addContextAssets();
return `${katexBlock(tokens[idx].content)}\n`;
};
md.inline.ruler.after('escape', 'math_inline', math_inline);
md.block.ruler.after('blockquote', 'math_block', math_block, {
alt: ['paragraph', 'reference', 'blockquote', 'list'],
});
md.renderer.rules.math_inline = inlineRenderer;
md.renderer.rules.math_block = blockRenderer;
};
const inlineRenderer = function(tokens:any[], idx:number) {
return katexInline(tokens[idx].content);
};
const katexBlock = function(latex:string) {
katexOptions.displayMode = true;
try {
return `<div class="joplin-editable"><pre class="joplin-source" data-joplin-language="katex" data-joplin-source-open="$$&#10;" data-joplin-source-close="&#10;$$&#10;">${markdownIt.utils.escapeHtml(latex)}</pre>${renderToStringWithCache(latex, katexOptions)}</div>`;
} catch (error) {
console.error('Katex error for:', latex, error);
return latex;
}
};
const blockRenderer = function(tokens:any[], idx:number) {
return `${katexBlock(tokens[idx].content)}\n`;
};
markdownIt.inline.ruler.after('escape', 'math_inline', math_inline);
markdownIt.block.ruler.after('blockquote', 'math_block', math_block, {
alt: ['paragraph', 'reference', 'blockquote', 'list'],
});
markdownIt.renderer.rules.math_inline = inlineRenderer;
markdownIt.renderer.rules.math_block = blockRenderer;
},
assets: katexStyle,
};

View File

@ -1,18 +1,13 @@
import { RuleOptions } from "lib/joplin-renderer/MdToHtml";
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = new Entities().encode;
const utils = require('../../utils');
const urlUtils = require('../../urlUtils.js');
const { getClassNameForMimeType } = require('font-awesome-filetypes');
function installRule(markdownIt, mdOptions, ruleOptions) {
const pluginOptions = {
// linkRenderingType = 1 is the regular rendering and clicking on it is handled via embedded JS (in onclick attribute)
// linkRenderingType = 2 gives a plain link with no JS. Caller needs to handle clicking on the link.
linkRenderingType: 1,
...ruleOptions.plugins['link_open'],
};
markdownIt.renderer.rules.link_open = function(tokens, idx) {
function plugin(markdownIt:any, ruleOptions:RuleOptions) {
markdownIt.renderer.rules.link_open = function(tokens:any[], idx:number) {
const token = tokens[idx];
let href = utils.getAttr(token.attrs, 'href');
const resourceHrefInfo = urlUtils.parseResourceUrl(href);
@ -65,12 +60,10 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
let js = `${ruleOptions.postMessageSyntax}(${JSON.stringify(href)}, { resourceId: ${JSON.stringify(resourceId)} }); return false;`;
if (ruleOptions.enableLongPress && !!resourceId) {
const longPressDelay = ruleOptions.longPressDelay ? ruleOptions.longPressDelay : 500;
const onClick = `${ruleOptions.postMessageSyntax}(${JSON.stringify(href)})`;
const onLongClick = `${ruleOptions.postMessageSyntax}("longclick:${resourceId}")`;
const touchStart = `t=setTimeout(()=>{t=null; ${onLongClick};}, ${longPressDelay});`;
const touchStart = `t=setTimeout(()=>{t=null; ${onLongClick};}, ${ruleOptions.longPressDelay});`;
const touchEnd = `if (!!t) {clearTimeout(t); t=null; ${onClick};}`;
js = `ontouchstart='${touchStart}' ontouchend='${touchEnd}'`;
@ -80,7 +73,7 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
if (hrefAttr.indexOf('#') === 0 && href.indexOf('#') === 0) js = ''; // If it's an internal anchor, don't add any JS since the webview is going to handle navigating to the right place
if (ruleOptions.plainResourceRendering || pluginOptions.linkRenderingType === 2) {
if (ruleOptions.plainResourceRendering || ruleOptions.linkRenderingType === 2) {
return `<a data-from-md ${resourceIdAttr} title='${htmlentities(title)}' href='${htmlentities(href)}' type='${htmlentities(mime)}'>`;
} else {
return `<a data-from-md ${resourceIdAttr} title='${htmlentities(title)}' href='${hrefAttr}' ${js} type='${htmlentities(mime)}'>${icon}`;
@ -88,9 +81,4 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
};
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};
export default { plugin };

View File

@ -1,49 +1,35 @@
function style() {
return [
{ name: 'mermaid.min.js' },
{ name: 'mermaid_render.js' },
{
inline: true,
// Note: Mermaid is buggy when rendering below a certain width (500px?)
// so set an arbitrarily high width here for the container. Once the
// diagram is rendered it will be reset to 100% in mermaid_render.js
text: '.mermaid { background-color: white; width: 640px; }',
mime: 'text/css',
},
];
}
function addContextAssets(context:any) {
if ('mermaid' in context.pluginAssets) return;
context.pluginAssets['mermaid'] = style();
}
// @ts-ignore: Keep the function signature as-is despite unusued arguments
function installRule(markdownIt:any, mdOptions:any, ruleOptions:any, context:any) {
const defaultRender:Function = markdownIt.renderer.rules.fence || function(tokens:any[], idx:number, options:any, env:any, self:any) {
return self.renderToken(tokens, idx, options, env, self);
};
markdownIt.renderer.rules.fence = function(tokens:any[], idx:number, options:{}, env:any, self:any) {
const token = tokens[idx];
if (token.info !== 'mermaid') return defaultRender(tokens, idx, options, env, self);
addContextAssets(context);
const contentHtml = markdownIt.utils.escapeHtml(token.content);
return `
<div class="joplin-editable">
<pre class="joplin-source" data-joplin-language="mermaid" data-joplin-source-open="\`\`\`mermaid&#10;" data-joplin-source-close="&#10;\`\`\`&#10;">${contentHtml}</pre>
<div class="mermaid">${contentHtml}</div>
</div>
`;
};
}
export default {
install: function(context:any, ruleOptions:any) {
return function(md:any, mdOptions:any) {
installRule(md, mdOptions, ruleOptions, context);
assets: function() {
return [
{ name: 'mermaid.min.js' },
{ name: 'mermaid_render.js' },
{
inline: true,
// Note: Mermaid is buggy when rendering below a certain width (500px?)
// so set an arbitrarily high width here for the container. Once the
// diagram is rendered it will be reset to 100% in mermaid_render.js
text: '.mermaid { background-color: white; width: 640px; }',
mime: 'text/css',
},
];
},
plugin: function(markdownIt:any) {
const defaultRender:Function = markdownIt.renderer.rules.fence || function(tokens:any[], idx:number, options:any, env:any, self:any) {
return self.renderToken(tokens, idx, options, env, self);
};
markdownIt.renderer.rules.fence = function(tokens:any[], idx:number, options:{}, env:any, self:any) {
const token = tokens[idx];
if (token.info !== 'mermaid') return defaultRender(tokens, idx, options, env, self);
const contentHtml = markdownIt.utils.escapeHtml(token.content);
return `
<div class="joplin-editable">
<pre class="joplin-source" data-joplin-language="mermaid" data-joplin-source-open="\`\`\`mermaid&#10;" data-joplin-source-close="&#10;\`\`\`&#10;">${contentHtml}</pre>
<div class="mermaid">${contentHtml}</div>
</div>
`;
};
},
assets: style,
};

View File

@ -1,53 +1,50 @@
import { RuleOptions } from 'lib/joplin-renderer/MdToHtml';
const md5 = require('md5');
const htmlUtils = require('../../htmlUtils');
// @ts-ignore: Keep the function signature as-is despite unusued arguments
function installRule(markdownIt:any, mdOptions:any, ruleOptions:any, context:any) {
markdownIt.core.ruler.push('sanitize_html', (state:any) => {
const tokens = state.tokens;
export default {
plugin: function(markdownIt:any, ruleOptions:RuleOptions) {
markdownIt.core.ruler.push('sanitize_html', (state:any) => {
const tokens = state.tokens;
const walkHtmlTokens = (tokens:any[]) => {
if (!tokens || !tokens.length) return;
const walkHtmlTokens = (tokens:any[]) => {
if (!tokens || !tokens.length) return;
for (const token of tokens) {
if (!['html_block', 'html_inline'].includes(token.type)) {
for (const token of tokens) {
if (!['html_block', 'html_inline'].includes(token.type)) {
walkHtmlTokens(token.children);
continue;
}
const cacheKey = md5(escape(token.content));
let sanitizedContent = ruleOptions.context.cache.value(cacheKey);
// For html_inline, the content is only a fragment of HTML, as it will be rendered, but
// it's not necessarily valid HTML. For example this HTML:
//
// <a href="#">Testing</a>
//
// will be rendered as three tokens:
//
// html_inline: <a href="#">
// text: Testing
// html_inline: </a>
//
// So the sanitizeHtml function must handle this kind of non-valid HTML.
if (!sanitizedContent) {
sanitizedContent = htmlUtils.sanitizeHtml(token.content, { addNoMdConvClass: true });
}
token.content = sanitizedContent;
ruleOptions.context.cache.setValue(cacheKey, sanitizedContent, 1000 * 60 * 60);
walkHtmlTokens(token.children);
continue;
}
};
const cacheKey = md5(escape(token.content));
let sanitizedContent = context.cache.value(cacheKey);
// For html_inline, the content is only a fragment of HTML, as it will be rendered, but
// it's not necessarily valid HTML. For example this HTML:
//
// <a href="#">Testing</a>
//
// will be rendered as three tokens:
//
// html_inline: <a href="#">
// text: Testing
// html_inline: </a>
//
// So the sanitizeHtml function must handle this kind of non-valid HTML.
if (!sanitizedContent) {
sanitizedContent = htmlUtils.sanitizeHtml(token.content, { addNoMdConvClass: true });
}
token.content = sanitizedContent;
context.cache.setValue(cacheKey, sanitizedContent, 1000 * 60 * 60);
walkHtmlTokens(token.children);
}
};
walkHtmlTokens(tokens);
});
}
export default function(context:any, ruleOptions:any) {
return function(md:any, mdOptions:any) {
installRule(md, mdOptions, ruleOptions, context);
};
}
walkHtmlTokens(tokens);
});
},
};

View File

@ -322,20 +322,36 @@ export enum ContentScriptType {
* Registers a new Markdown-It plugin, which should follow this template:
*
* ```javascript
* // The module should export a function that takes a `pluginContext` as argument (currently unused)
* module.exports = function(pluginContext) {
* // That function should return an object with a number of properties:
* return {
* // Required:
* install: function(context, ruleOptions) {
* return function(md, mdOptions) {
* installRule(md, mdOptions, ruleOptions, context);
* };
*
* // The module should export an object like below:
*
* module.exports = {
* default: {
* // This is the actual Markdown-It plugin - check the [official doc](https://github.com/markdown-it/markdown-it) for more information
* // The `options` parameter is of type [RuleOptions](https://github.com/laurent22/joplin/blob/dev/ReactNativeClient/lib/joplin-renderer/MdToHtml.ts), which
* // contains a number of options, mostly useful for Joplin's internal code.
* plugin: function(markdownIt, options) {
*
* },
*
* // You may also specify additional assets such as JS or CSS that should be loaded in the rendered HTML document.
* // Check for example the Joplin [Mermaid plugin](https://github.com/laurent22/joplin/blob/dev/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.ts) to
* // see how the data should be structured.
* assets: {},
* }
* }
* ```
*
* To include a regular Markdown-It plugin, that doesn't make use of any Joplin-specific feature, you
* would simply create a file such as this:
*
* ```javascript
* module.exports = {
* default: {
* plugin: require('markdown-it-toc-done-right');
* }
* }
* ```
*/
MarkdownItPlugin = 'markdownItPlugin',
CodeMirrorPlugin = 'codeMirrorPlugin',

View File

@ -1,8 +0,0 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
const uuid_1 = require('lib/uuid');
function viewIdGen(plugin) {
return `plugin-view-${plugin.id}-${uuid_1.default.createNano()}`;
}
exports.default = viewIdGen;
// # sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmlld0lkR2VuLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsidmlld0lkR2VuLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQ0EsbUNBQTRCO0FBRTVCLFNBQXdCLFNBQVMsQ0FBQyxNQUFhO0lBQzlDLE9BQU8sZUFBZSxNQUFNLENBQUMsRUFBRSxJQUFJLGNBQUksQ0FBQyxVQUFVLEVBQUUsRUFBRSxDQUFBO0FBQ3ZELENBQUM7QUFGRCw0QkFFQyJ9

View File

@ -393,6 +393,7 @@ function themeStyle(themeId:number) {
output = Object.assign({}, globalStyle, fontSizes, themes[themeId]);
output = addMissingProperties(output);
output = addExtraStyles(output);
output.cacheKey = cacheKey;
themeCache_[cacheKey] = output;
return themeCache_[cacheKey];