From fe90d92e01427bd2b38200393713ea28763507a9 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Tue, 20 Oct 2020 17:52:02 +0100 Subject: [PATCH] Simplified how Markdown-It plugins are created --- CliClient/tests/MdToHtml.js | 2 +- .../markdownItPluginTest.js | 2 +- .../src/markdownItTestPlugin.js | 2 +- .../NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx | 2 +- .../services/plugins/UserWebview.tsx | 2 +- .../services/plugins/hooks/useThemeCss.ts | 2 +- .../lib/joplin-renderer/MdToHtml.ts | 59 ++++++++++++++--- .../MdToHtml/rules/checkbox.ts | 64 ++++++------------- .../lib/services/plugins/api/JoplinPlugins.ts | 11 ++++ .../lib/services/plugins/api/types.ts | 19 ++++++ ReactNativeClient/lib/theme.ts | 12 ++-- 11 files changed, 110 insertions(+), 67 deletions(-) diff --git a/CliClient/tests/MdToHtml.js b/CliClient/tests/MdToHtml.js index a4e3cce6bb..0b8f1174eb 100644 --- a/CliClient/tests/MdToHtml.js +++ b/CliClient/tests/MdToHtml.js @@ -59,7 +59,7 @@ describe('MdToHtml', function() { if (mdFilename === 'checkbox_alternative.md') { mdToHtmlOptions.plugins = { checkbox: { - renderingType: 2, + checkboxRenderingType: 2, }, }; } diff --git a/CliClient/tests/support/pluginContentScripts/markdownItPluginTest.js b/CliClient/tests/support/pluginContentScripts/markdownItPluginTest.js index 8a9ec4801a..0d508529de 100644 --- a/CliClient/tests/support/pluginContentScripts/markdownItPluginTest.js +++ b/CliClient/tests/support/pluginContentScripts/markdownItPluginTest.js @@ -17,6 +17,6 @@ module.exports = function(pluginContext) { installRule(md, mdOptions, ruleOptions, context); }; }, - style: {}, + assets: {}, } } diff --git a/CliClient/tests/support/plugins/content_script/src/markdownItTestPlugin.js b/CliClient/tests/support/plugins/content_script/src/markdownItTestPlugin.js index 8a9ec4801a..0d508529de 100644 --- a/CliClient/tests/support/plugins/content_script/src/markdownItTestPlugin.js +++ b/CliClient/tests/support/plugins/content_script/src/markdownItTestPlugin.js @@ -17,6 +17,6 @@ module.exports = function(pluginContext) { installRule(md, mdOptions, ruleOptions, context); }; }, - style: {}, + assets: {}, } } diff --git a/ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx b/ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx index 5e6331f06a..1b8dce10c7 100644 --- a/ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx +++ b/ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx @@ -27,7 +27,7 @@ function markupRenderOptions(override:any = null) { return { plugins: { checkbox: { - renderingType: 2, + checkboxRenderingType: 2, }, link_open: { linkRenderingType: 2, diff --git a/ElectronClient/services/plugins/UserWebview.tsx b/ElectronClient/services/plugins/UserWebview.tsx index c19115673d..653b7b0612 100644 --- a/ElectronClient/services/plugins/UserWebview.tsx +++ b/ElectronClient/services/plugins/UserWebview.tsx @@ -10,7 +10,7 @@ export interface Props { onMessage:Function, pluginId:string, viewId:string, - themeId:string, + themeId:number, minWidth?: number, minHeight?: number, fitToContent?: boolean, diff --git a/ElectronClient/services/plugins/hooks/useThemeCss.ts b/ElectronClient/services/plugins/hooks/useThemeCss.ts index adf7eefaf9..e23d0fc6e3 100644 --- a/ElectronClient/services/plugins/hooks/useThemeCss.ts +++ b/ElectronClient/services/plugins/hooks/useThemeCss.ts @@ -6,7 +6,7 @@ const { camelCaseToDash, formatCssSize } = require('lib/string-utils'); interface HookDependencies { pluginId: string, - themeId: string, + themeId: number, } function themeToCssVariables(theme:any) { diff --git a/ReactNativeClient/lib/joplin-renderer/MdToHtml.ts b/ReactNativeClient/lib/joplin-renderer/MdToHtml.ts index 0f089bff4b..5018515ebd 100644 --- a/ReactNativeClient/lib/joplin-renderer/MdToHtml.ts +++ b/ReactNativeClient/lib/joplin-renderer/MdToHtml.ts @@ -8,6 +8,8 @@ const md5 = require('md5'); interface RendererRule { install(context:any, ruleOptions:any):any, assets?(theme:any):any, + rule?: any, // TODO: remove + plugin?: any, } interface RendererRules { @@ -113,6 +115,15 @@ interface RenderResult { cssStrings: string[], } +export interface RuleOptions { + context: PluginContext, + theme: any, + postMessageSyntax: string, + + // Used by checkboxes to specify how it should be rendered + checkboxRenderingType?: number, +} + export default class MdToHtml { private resourceBaseUrl_:string; @@ -235,6 +246,21 @@ export default class MdToHtml { }; } + private allUnprocessedAssets(theme:any) { + const assets:any = {}; + for (const key in rules) { + if (!this.pluginEnabled(key)) continue; + const rule = rules[key]; + + if (rule.assets) { + assets[key] = rule.assets(theme); + } + } + + return assets; + } + + // TODO: remove async allAssets(theme:any) { const assets:any = {}; for (const key in rules) { @@ -303,17 +329,18 @@ export default class MdToHtml { const cachedOutput = this.cachedOutputs_[cacheKey]; if (cachedOutput) return cachedOutput; - const context:PluginContext = { - css: {}, - pluginAssets: {}, - cache: this.contextCache_, - }; - const ruleOptions = Object.assign({}, options, { resourceBaseUrl: this.resourceBaseUrl_, ResourceModel: this.ResourceModel_, }); + const context:PluginContext = { + css: {}, + pluginAssets: {}, + cache: this.contextCache_, + // options: ruleOptions, + }; + const markdownIt = new MarkdownIt({ breaks: !this.pluginEnabled('softbreaks'), typographer: this.pluginEnabled('typographer'), @@ -391,8 +418,18 @@ export default class MdToHtml { for (const key in allRules) { if (!this.pluginEnabled(key)) continue; const rule = allRules[key]; - const ruleInstall:Function = rule.install ? rule.install : (rule as any); - markdownIt.use(ruleInstall(context, { ...ruleOptions })); + 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 })); + } } markdownIt.use(markdownItAnchor, { slugify: slugify }); @@ -410,11 +447,13 @@ export default class MdToHtml { setupLinkify(markdownIt); - const renderedBody = markdownIt.render(body); + const renderedBody = markdownIt.render(body, context); + + const pluginAssets = this.allUnprocessedAssets(theme); let cssStrings = noteStyle(options.theme); - let output = this.processPluginAssets(context.pluginAssets); + let output = this.processPluginAssets(pluginAssets); // context.pluginAssets); cssStrings = cssStrings.concat(output.cssStrings); if (options.userCss) cssStrings.push(options.userCss); diff --git a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.ts b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.ts index 3b96098f48..7633c2453d 100644 --- a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.ts +++ b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.ts @@ -1,24 +1,21 @@ +import { RuleOptions } from '../../MdToHtml'; + let checkboxIndex_ = -1; -const pluginAssets:Function[] = []; - -pluginAssets[1] = function() { +function pluginAssets(theme:any) { return [ { inline: true, mime: 'text/css', text: ` + /* + FOR THE MARKDOWN EDITOR + */ + /* Remove the indentation from the checkboxes at the root of the document (otherwise they are too far right), but keep it for their children to allow nested lists. Make sure this value matches the UL margin. */ - /* - .md-checkbox .checkbox-wrapper { - display: flex; - align-items: center; - } - */ - li.md-checkbox { list-style-type: none; } @@ -26,30 +23,16 @@ pluginAssets[1] = function() { li.md-checkbox input[type=checkbox] { margin-left: -1.71em; margin-right: 0.7em; - }`, - }, - ]; -}; - -pluginAssets[2] = function(theme:any) { - return [ - { - inline: true, - mime: 'text/css', - text: ` - /* https://stackoverflow.com/questions/7478336/only-detect-click-event-on-pseudo-element#comment39751366_7478344 */ - /* Not doing this trick anymore. See Modules/TinyMCE/JoplinLists/src/main/ts/ui/Buttons.ts */ - - /* - ul.joplin-checklist li { - pointer-events: none; } - */ - + ul.joplin-checklist { list-style:none; } + /* + FOR THE RICH TEXT EDITOR + */ + ul.joplin-checklist li::before { content:"\\f14a"; font-family:"Font Awesome 5 Free"; @@ -68,7 +51,7 @@ pluginAssets[2] = function(theme:any) { }`, }, ]; -}; +} function createPrefixTokens(Token:any, id:string, checked:boolean, label:string, postMessageSyntax:string, sourceToken:any):any[] { let token = null; @@ -129,9 +112,8 @@ function createSuffixTokens(Token:any):any[] { ]; } -// @ts-ignore: Keep the function signature as-is despite unusued arguments -function installRule(markdownIt:any, mdOptions:any, ruleOptions:any, context:any) { - const pluginOptions = { renderingType: 1, ...ruleOptions.plugins['checkbox'] }; +function checkboxPlugin(markdownIt:any, options:RuleOptions) { + const renderingType = options.checkboxRenderingType || 1; markdownIt.core.ruler.push('checkbox', (state:any) => { const tokens = state.tokens; @@ -180,14 +162,14 @@ function installRule(markdownIt:any, mdOptions:any, ruleOptions:any, context:any const currentList = lists[lists.length - 1]; - if (pluginOptions.renderingType === 1) { + if (renderingType === 1) { checkboxIndex_++; const id = `md-checkbox-${checkboxIndex_}`; // Prepend the text content with the checkbox markup and the opening tag at the end of the text content. - const prefix = createPrefixTokens(Token, id, checked, label, ruleOptions.postMessageSyntax, token); + const prefix = createPrefixTokens(Token, id, checked, label, options.postMessageSyntax, token); const suffix = createSuffixTokens(Token); token.children = markdownIt.utils.arrayReplaceAt(token.children, 0, prefix); @@ -214,20 +196,12 @@ function installRule(markdownIt:any, mdOptions:any, ruleOptions:any, context:any currentListItem.attrSet('class', (`${currentListItem.attrGet('class') || ''} checked`).trim()); } } - - if (!('checkbox' in context.pluginAssets)) { - context.pluginAssets['checkbox'] = pluginAssets[pluginOptions.renderingType](ruleOptions.theme); - } } } }); } export default { - install: function(context:any, ruleOptions:any) { - return function(md:any, mdOptions:any) { - installRule(md, mdOptions, ruleOptions, context); - }; - }, - assets: pluginAssets[2], + plugin: checkboxPlugin, + assets: pluginAssets, }; diff --git a/ReactNativeClient/lib/services/plugins/api/JoplinPlugins.ts b/ReactNativeClient/lib/services/plugins/api/JoplinPlugins.ts index 5f4c070330..0ce47f3ca3 100644 --- a/ReactNativeClient/lib/services/plugins/api/JoplinPlugins.ts +++ b/ReactNativeClient/lib/services/plugins/api/JoplinPlugins.ts @@ -49,6 +49,17 @@ export default class JoplinPlugins { } } + /** + * Registers a new content script. Unlike regular plugin code, which runs in a separate process, content scripts run within the main process code + * and thus allow improved performances and more customisations in specific cases. It can be used for example to load a Markdown or editor plugin. + * + * Note that registering a content script in itself will do nothing - it will only be loaded in specific cases by the relevant app modules + * (eg. the Markdown renderer or the code editor). So it is not a way to inject and run arbitrary code in the app, which for safety and performance reasons is not supported. + * + * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/CliClient/tests/support/plugins/content_script) + * + * @param scriptPath Must be a path relative to the plugin main script. For example, if your file content_script.js is next to your index.ts file, you would set `scriptPath` to `"./content_script.js`. + */ async registerContentScript(type:ContentScriptType, id:string, scriptPath:string) { return this.plugin.registerContentScript(type, id, scriptPath); } diff --git a/ReactNativeClient/lib/services/plugins/api/types.ts b/ReactNativeClient/lib/services/plugins/api/types.ts index b8f5c062c3..a5bd0a1051 100644 --- a/ReactNativeClient/lib/services/plugins/api/types.ts +++ b/ReactNativeClient/lib/services/plugins/api/types.ts @@ -318,6 +318,25 @@ export type Path = string[]; // ================================================================= 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); + * }; + * }, + * assets: {}, + * } + * } + * ``` + */ MarkdownItPlugin = 'markdownItPlugin', CodeMirrorPlugin = 'codeMirrorPlugin', } diff --git a/ReactNativeClient/lib/theme.ts b/ReactNativeClient/lib/theme.ts index 0e26bd9588..c2d26841da 100644 --- a/ReactNativeClient/lib/theme.ts +++ b/ReactNativeClient/lib/theme.ts @@ -8,8 +8,8 @@ import theme_solarizedDark from './themes/solarizedDark'; import theme_nord from './themes/nord'; import theme_aritimDark from './themes/aritimDark'; import theme_oledDark from './themes/oledDark'; +import Setting from 'lib/models/Setting'; -const Setting = require('lib/models/Setting').default; const Color = require('color'); const themes:any = { @@ -364,13 +364,13 @@ function addExtraStyles(style:any) { const themeCache_:any = {}; -function themeStyle(theme:any) { - if (!theme) throw new Error('Theme must be specified'); +function themeStyle(themeId:number) { + if (!themeId) throw new Error('Theme must be specified'); const zoomRatio = 1; // Setting.value('style.zoom') / 100; const editorFontSize = Setting.value('style.editor.fontSize'); - const cacheKey = [theme, zoomRatio, editorFontSize].join('-'); + const cacheKey = [themeId, zoomRatio, editorFontSize].join('-'); if (themeCache_[cacheKey]) return themeCache_[cacheKey]; // Font size are not theme specific, but they must be referenced @@ -390,7 +390,7 @@ function themeStyle(theme:any) { // All theme are based on the light style, and just override the // relevant properties - output = Object.assign({}, globalStyle, fontSizes, themes[theme]); + output = Object.assign({}, globalStyle, fontSizes, themes[themeId]); output = addMissingProperties(output); output = addExtraStyles(output); @@ -406,7 +406,7 @@ const cachedStyles_:any = { // cacheKey must be a globally unique key, and must change whenever // the dependencies of the style change. If the style depends only // on the theme, a static string can be provided as a cache key. -function buildStyle(cacheKey:any, themeId:string, callback:Function) { +function buildStyle(cacheKey:any, themeId:number, callback:Function) { cacheKey = Array.isArray(cacheKey) ? cacheKey.join('_') : cacheKey; // We clear the cache whenever switching themes