diff --git a/.eslintignore b/.eslintignore index 6de6609abd..1013d938a7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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 diff --git a/.gitignore b/.gitignore index ee6a5c8618..57edcfa835 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.ignore b/.ignore index 547d944e41..caf2469fad 100644 --- a/.ignore +++ b/.ignore @@ -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 diff --git a/ElectronClient/gui/NoteEditor/utils/useMarkupToHtml.ts b/ElectronClient/gui/NoteEditor/utils/useMarkupToHtml.ts index 29436674b7..4bb42da8f7 100644 --- a/ElectronClient/gui/NoteEditor/utils/useMarkupToHtml.ts +++ b/ElectronClient/gui/NoteEditor/utils/useMarkupToHtml.ts @@ -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 => { 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]); } diff --git a/ReactNativeClient/lib/joplin-renderer/MarkupToHtml.ts b/ReactNativeClient/lib/joplin-renderer/MarkupToHtml.ts index 9b1bb2c1b9..d189ffb865 100644 --- a/ReactNativeClient/lib/joplin-renderer/MarkupToHtml.ts +++ b/ReactNativeClient/lib/joplin-renderer/MarkupToHtml.ts @@ -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 ''; diff --git a/ReactNativeClient/lib/joplin-renderer/MdToHtml.ts b/ReactNativeClient/lib/joplin-renderer/MdToHtml.ts index 5018515ebd..afc0515ae8 100644 --- a/ReactNativeClient/lib/joplin-renderer/MdToHtml.ts +++ b/ReactNativeClient/lib/joplin-renderer/MdToHtml.ts @@ -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) { //

\n if (html.length < 8) return html; if (html.substr(0, 3) !== '

') 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 ''; - } } diff --git a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/code_inline.js b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/code_inline.ts similarity index 56% rename from ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/code_inline.js rename to ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/code_inline.ts index c0711d2d27..1db54f8f95 100644 --- a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/code_inline.js +++ b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/code_inline.ts @@ -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, }; diff --git a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.ts b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.ts index f3a410089a..fd36a390fb 100644 --- a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.ts +++ b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.ts @@ -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, +}; + diff --git a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fountain.js b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fountain.ts similarity index 80% rename from ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fountain.js rename to ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fountain.ts index e8bfe04626..bf1924dc69 100644 --- a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fountain.js +++ b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fountain.ts @@ -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, }; diff --git a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/highlight_keywords.js b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/highlight_keywords.ts similarity index 78% rename from ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/highlight_keywords.js rename to ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/highlight_keywords.ts index d67bbd700c..c33f46bd93 100644 --- a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/highlight_keywords.js +++ b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/highlight_keywords.ts @@ -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, +} diff --git a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/html_image.js b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/html_image.ts similarity index 68% rename from ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/html_image.js rename to ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/html_image.ts index d3e41a9a22..0ebfb20f7a 100644 --- a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/html_image.js +++ b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/html_image.ts @@ -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 ``; 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 = //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 ``; 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 } diff --git a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/image.js b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/image.ts similarity index 75% rename from ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/image.js rename to ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/image.ts index 2c2e056908..394d3358df 100644 --- a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/image.js +++ b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/image.ts @@ -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 }; \ No newline at end of file diff --git a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/katex.js b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/katex.ts similarity index 67% rename from ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/katex.js rename to ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/katex.ts index f4895e2471..9533690b1a 100644 --- a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/katex.js +++ b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/katex.ts @@ -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 `${markdownIt.utils.escapeHtml(latex)}${renderToStringWithCache(latex, katexOptions)}`; + } 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 `${md.utils.escapeHtml(latex)}${renderToStringWithCache(latex, options)}`; - } 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 `

${md.utils.escapeHtml(latex)}
${renderToStringWithCache(latex, options)}
`; - } 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 `
${markdownIt.utils.escapeHtml(latex)}
${renderToStringWithCache(latex, katexOptions)}
`; + } 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, }; diff --git a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/link_open.js b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/link_open.ts similarity index 81% rename from ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/link_open.js rename to ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/link_open.ts index b4d8c7c847..338cd7f5eb 100644 --- a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/link_open.js +++ b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/link_open.ts @@ -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 ``; } else { return `${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 }; \ No newline at end of file diff --git a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.ts b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.ts index a29463767f..a4e13af073 100644 --- a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.ts +++ b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.ts @@ -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 ` -
-
${contentHtml}
-
${contentHtml}
-
- `; - }; -} - 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 ` +
+
${contentHtml}
+
${contentHtml}
+
+ `; }; }, - assets: style, }; diff --git a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.ts b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.ts index b00c7392d4..4a1151632b 100644 --- a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.ts +++ b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.ts @@ -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: + // + //
Testing + // + // will be rendered as three tokens: + // + // html_inline: + // text: Testing + // html_inline: + // + // 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: - // - // Testing - // - // will be rendered as three tokens: - // - // html_inline: - // text: Testing - // html_inline: - // - // 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); + }); + }, +}; diff --git a/ReactNativeClient/lib/services/plugins/api/types.ts b/ReactNativeClient/lib/services/plugins/api/types.ts index a5bd0a1051..229311d0cf 100644 --- a/ReactNativeClient/lib/services/plugins/api/types.ts +++ b/ReactNativeClient/lib/services/plugins/api/types.ts @@ -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', diff --git a/ReactNativeClient/lib/services/plugins/utils/viewIdGen.js b/ReactNativeClient/lib/services/plugins/utils/viewIdGen.js deleted file mode 100644 index 4a0f7d488b..0000000000 --- a/ReactNativeClient/lib/services/plugins/utils/viewIdGen.js +++ /dev/null @@ -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 diff --git a/ReactNativeClient/lib/theme.ts b/ReactNativeClient/lib/theme.ts index c2d26841da..e1f569cad0 100644 --- a/ReactNativeClient/lib/theme.ts +++ b/ReactNativeClient/lib/theme.ts @@ -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];