1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Plugins: Add support for getting plugin settings from a Markdown renderer

This commit is contained in:
Laurent Cozic 2023-11-03 19:45:21 +00:00
parent b097ab29ee
commit 8be22ed910
12 changed files with 105 additions and 44 deletions

View File

@ -206,7 +206,7 @@ describe('services_PluginService', () => {
const mdToHtml = new MdToHtml(); const mdToHtml = new MdToHtml();
const module = require(contentScript.path).default; const module = require(contentScript.path).default;
mdToHtml.loadExtraRendererRule(contentScript.id, tempDir, module({})); mdToHtml.loadExtraRendererRule(contentScript.id, tempDir, module({}), '');
const result = await mdToHtml.render([ const result = await mdToHtml.render([
'```justtesting', '```justtesting',

View File

@ -48,6 +48,7 @@ import ItemChange from '@joplin/lib/models/ItemChange';
import PlainEditor from './NoteBody/PlainEditor/PlainEditor'; import PlainEditor from './NoteBody/PlainEditor/PlainEditor';
import CodeMirror6 from './NoteBody/CodeMirror/v6/CodeMirror'; import CodeMirror6 from './NoteBody/CodeMirror/v6/CodeMirror';
import CodeMirror5 from './NoteBody/CodeMirror/v5/CodeMirror'; import CodeMirror5 from './NoteBody/CodeMirror/v5/CodeMirror';
import { namespacedKey } from '@joplin/lib/services/plugins/api/JoplinSettings';
const commands = [ const commands = [
require('./commands/showRevisions'), require('./commands/showRevisions'),
@ -159,10 +160,15 @@ function NoteEditor(props: NoteEditorProps) {
return formNote.saveActionQueue.waitForAllDone(); return formNote.saveActionQueue.waitForAllDone();
} }
const settingValue = useCallback((pluginId: string, key: string) => {
return Setting.value(namespacedKey(pluginId, key));
}, []);
const markupToHtml = useMarkupToHtml({ const markupToHtml = useMarkupToHtml({
themeId: props.themeId, themeId: props.themeId,
customCss: props.customCss, customCss: props.customCss,
plugins: props.plugins, plugins: props.plugins,
settingValue,
}); });
const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null): Promise<any[]> => { const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null): Promise<any[]> => {

View File

@ -12,6 +12,7 @@ interface HookDependencies {
themeId: number; themeId: number;
customCss: string; customCss: string;
plugins: PluginStates; plugins: PluginStates;
settingValue: (pluginId: string, key: string)=> any;
} }
export interface MarkupToHtmlOptions { export interface MarkupToHtmlOptions {
@ -59,12 +60,16 @@ export default function useMarkupToHtml(deps: HookDependencies) {
delete options.replaceResourceInternalToExternalLinks; delete options.replaceResourceInternalToExternalLinks;
const result = await markupToHtml.render(markupLanguage, md, theme, { codeTheme: theme.codeThemeCss, const result = await markupToHtml.render(markupLanguage, md, theme, {
codeTheme: theme.codeThemeCss,
resources: resources, resources: resources,
postMessageSyntax: 'ipcProxySendToHost', postMessageSyntax: 'ipcProxySendToHost',
splitted: true, splitted: true,
externalAssetsOnly: true, externalAssetsOnly: true,
codeHighlightCacheKey: 'useMarkupToHtml', ...options }); codeHighlightCacheKey: 'useMarkupToHtml',
settingValue: deps.settingValue,
...options,
});
return result; return result;
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied

View File

@ -5,8 +5,8 @@
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
TEMP_PATH=~/src/plugin-tests TEMP_PATH=~/src/plugin-tests
NEED_COMPILING=0 NEED_COMPILING=1
PLUGIN_PATH=~/src/joplin/packages/app-cli/tests/support/plugins/simple PLUGIN_PATH=~/src/plugin-abc
if [[ $NEED_COMPILING == 1 ]]; then if [[ $NEED_COMPILING == 1 ]]; then
mkdir -p "$TEMP_PATH" mkdir -p "$TEMP_PATH"

View File

@ -7,6 +7,7 @@ export interface ChangeEvent {
keys: string[]; keys: string[];
} }
export type ChangeHandler = (event: ChangeEvent) => void; export type ChangeHandler = (event: ChangeEvent) => void;
export declare const namespacedKey: (pluginId: string, key: string) => string;
/** /**
* This API allows registering new settings and setting sections, as well as getting and setting settings. Once a setting has been registered it will appear in the config screen and be editable by the user. * This API allows registering new settings and setting sections, as well as getting and setting settings. Once a setting has been registered it will appear in the config screen and be editable by the user.
* *
@ -19,8 +20,6 @@ export type ChangeHandler = (event: ChangeEvent) => void;
export default class JoplinSettings { export default class JoplinSettings {
private plugin_; private plugin_;
constructor(plugin: Plugin); constructor(plugin: Plugin);
private get keyPrefix();
private namespacedKey;
/** /**
* Registers new settings. * Registers new settings.
* Note that registering a setting item is dynamic and will be gone next time Joplin starts. * Note that registering a setting item is dynamic and will be gone next time Joplin starts.

View File

@ -519,6 +519,20 @@ export interface ContentScriptContext {
postMessage: PostMessageHandler; postMessage: PostMessageHandler;
} }
export interface ContentScriptModuleLoadedEvent {
userData?: any;
}
export interface ContentScriptModule {
onLoaded?: (event:ContentScriptModuleLoadedEvent) => void;
plugin: () => any;
assets?: () => void;
}
export interface MarkdownItContentScriptModule extends Omit<ContentScriptModule, 'plugin'> {
plugin: (markdownIt:any, options:any) => any;
}
export enum ContentScriptType { export enum ContentScriptType {
/** /**
* Registers a new Markdown-It plugin, which should follow the template * Registers a new Markdown-It plugin, which should follow the template
@ -528,7 +542,7 @@ export enum ContentScriptType {
* module.exports = { * module.exports = {
* default: function(context) { * default: function(context) {
* return { * return {
* plugin: function(markdownIt, options) { * plugin: function(markdownIt, pluginOptions) {
* // ... * // ...
* }, * },
* assets: { * assets: {
@ -538,6 +552,7 @@ export enum ContentScriptType {
* } * }
* } * }
* ``` * ```
*
* See [the * See [the
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
* for a simple Markdown-it plugin example. * for a simple Markdown-it plugin example.
@ -550,10 +565,7 @@ export enum ContentScriptType {
* *
* - The **required** `plugin` key is the actual Markdown-It plugin - check * - The **required** `plugin` key is the actual Markdown-It plugin - check
* the [official doc](https://github.com/markdown-it/markdown-it) for more * the [official doc](https://github.com/markdown-it/markdown-it) for more
* information. The `options` parameter is of type * information.
* [RuleOptions](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml.ts),
* which contains a number of options, mostly useful for Joplin's internal
* code.
* *
* - Using the **optional** `assets` key you may specify assets such as JS * - Using the **optional** `assets` key you may specify assets such as JS
* or CSS that should be loaded in the rendered HTML document. Check for * or CSS that should be loaded in the rendered HTML document. Check for
@ -561,6 +573,11 @@ export enum ContentScriptType {
* plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts) * plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts)
* to see how the data should be structured. * to see how the data should be structured.
* *
* ## Getting the settings from the renderer
*
* You can access your plugin settings from the renderer by calling
* `pluginOptions.settingValue("your-setting-key')`.
*
* ## Posting messages from the content script to your plugin * ## Posting messages from the content script to your plugin
* *
* The application provides the following function to allow executing * The application provides the following function to allow executing

View File

@ -70,6 +70,17 @@ export interface ChangeEvent {
export type ChangeHandler = (event: ChangeEvent)=> void; export type ChangeHandler = (event: ChangeEvent)=> void;
const keyPrefix = (pluginId: string): string => {
return `plugin-${pluginId}.`;
};
// Ensures that the plugin settings and sections are within their own namespace,
// to prevent them from overwriting other plugin settings or the default
// settings.
export const namespacedKey = (pluginId: string, key: string): string => {
return `${keyPrefix(pluginId)}${key}`;
};
/** /**
* This API allows registering new settings and setting sections, as well as getting and setting settings. Once a setting has been registered it will appear in the config screen and be editable by the user. * This API allows registering new settings and setting sections, as well as getting and setting settings. Once a setting has been registered it will appear in the config screen and be editable by the user.
* *
@ -86,16 +97,6 @@ export default class JoplinSettings {
this.plugin_ = plugin; this.plugin_ = plugin;
} }
private get keyPrefix(): string {
return `plugin-${this.plugin_.id}.`;
}
// Ensures that the plugin settings and sections are within their own namespace, to prevent them from
// overwriting other plugin settings or the default settings.
private namespacedKey(key: string): string {
return `${this.keyPrefix}${key}`;
}
/** /**
* Registers new settings. * Registers new settings.
* Note that registering a setting item is dynamic and will be gone next time Joplin starts. * Note that registering a setting item is dynamic and will be gone next time Joplin starts.
@ -116,7 +117,7 @@ export default class JoplinSettings {
if ('subType' in setting) internalSettingItem.subType = setting.subType; if ('subType' in setting) internalSettingItem.subType = setting.subType;
if ('isEnum' in setting) internalSettingItem.isEnum = setting.isEnum; if ('isEnum' in setting) internalSettingItem.isEnum = setting.isEnum;
if ('section' in setting) internalSettingItem.section = this.namespacedKey(setting.section); if ('section' in setting) internalSettingItem.section = namespacedKey(this.plugin_.id, setting.section);
if ('options' in setting) internalSettingItem.options = () => setting.options; if ('options' in setting) internalSettingItem.options = () => setting.options;
if ('appTypes' in setting) internalSettingItem.appTypes = setting.appTypes; if ('appTypes' in setting) internalSettingItem.appTypes = setting.appTypes;
if ('secure' in setting) internalSettingItem.secure = setting.secure; if ('secure' in setting) internalSettingItem.secure = setting.secure;
@ -126,7 +127,7 @@ export default class JoplinSettings {
if ('step' in setting) internalSettingItem.step = setting.step; if ('step' in setting) internalSettingItem.step = setting.step;
if ('storage' in setting) internalSettingItem.storage = setting.storage; if ('storage' in setting) internalSettingItem.storage = setting.storage;
await Setting.registerSetting(this.namespacedKey(key), internalSettingItem); await Setting.registerSetting(namespacedKey(this.plugin_.id, key), internalSettingItem);
} }
} }
@ -150,21 +151,21 @@ export default class JoplinSettings {
* Registers a new setting section. Like for registerSetting, it is dynamic and needs to be done every time the plugin starts. * Registers a new setting section. Like for registerSetting, it is dynamic and needs to be done every time the plugin starts.
*/ */
public async registerSection(name: string, section: SettingSection) { public async registerSection(name: string, section: SettingSection) {
return Setting.registerSection(this.namespacedKey(name), SettingSectionSource.Plugin, section); return Setting.registerSection(namespacedKey(this.plugin_.id, name), SettingSectionSource.Plugin, section);
} }
/** /**
* Gets a setting value (only applies to setting you registered from your plugin) * Gets a setting value (only applies to setting you registered from your plugin)
*/ */
public async value(key: string): Promise<any> { public async value(key: string): Promise<any> {
return Setting.value(this.namespacedKey(key)); return Setting.value(namespacedKey(this.plugin_.id, key));
} }
/** /**
* Sets a setting value (only applies to setting you registered from your plugin) * Sets a setting value (only applies to setting you registered from your plugin)
*/ */
public async setValue(key: string, value: any) { public async setValue(key: string, value: any) {
return Setting.setValue(this.namespacedKey(key), value); return Setting.setValue(namespacedKey(this.plugin_.id, key), value);
} }
/** /**
@ -187,8 +188,8 @@ export default class JoplinSettings {
// Filter out keys that are not related to this plugin // Filter out keys that are not related to this plugin
eventManager.on('settingsChange', (event: ChangeEvent) => { eventManager.on('settingsChange', (event: ChangeEvent) => {
const keys = event.keys const keys = event.keys
.filter(k => k.indexOf(this.keyPrefix) === 0) .filter(k => k.indexOf(keyPrefix(this.plugin_.id)) === 0)
.map(k => k.substr(this.keyPrefix.length)); .map(k => k.substr(keyPrefix(this.plugin_.id).length));
if (!keys.length) return; if (!keys.length) return;
handler({ keys }); handler({ keys });
}); });

View File

@ -519,6 +519,20 @@ export interface ContentScriptContext {
postMessage: PostMessageHandler; postMessage: PostMessageHandler;
} }
export interface ContentScriptModuleLoadedEvent {
userData?: any;
}
export interface ContentScriptModule {
onLoaded?: (event: ContentScriptModuleLoadedEvent)=> void;
plugin: ()=> any;
assets?: ()=> void;
}
export interface MarkdownItContentScriptModule extends Omit<ContentScriptModule, 'plugin'> {
plugin: (markdownIt: any, options: any)=> any;
}
export enum ContentScriptType { export enum ContentScriptType {
/** /**
* Registers a new Markdown-It plugin, which should follow the template * Registers a new Markdown-It plugin, which should follow the template
@ -528,7 +542,7 @@ export enum ContentScriptType {
* module.exports = { * module.exports = {
* default: function(context) { * default: function(context) {
* return { * return {
* plugin: function(markdownIt, options) { * plugin: function(markdownIt, pluginOptions) {
* // ... * // ...
* }, * },
* assets: { * assets: {
@ -538,6 +552,7 @@ export enum ContentScriptType {
* } * }
* } * }
* ``` * ```
*
* See [the * See [the
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
* for a simple Markdown-it plugin example. * for a simple Markdown-it plugin example.
@ -550,10 +565,7 @@ export enum ContentScriptType {
* *
* - The **required** `plugin` key is the actual Markdown-It plugin - check * - The **required** `plugin` key is the actual Markdown-It plugin - check
* the [official doc](https://github.com/markdown-it/markdown-it) for more * the [official doc](https://github.com/markdown-it/markdown-it) for more
* information. The `options` parameter is of type * information.
* [RuleOptions](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml.ts),
* which contains a number of options, mostly useful for Joplin's internal
* code.
* *
* - Using the **optional** `assets` key you may specify assets such as JS * - Using the **optional** `assets` key you may specify assets such as JS
* or CSS that should be loaded in the rendered HTML document. Check for * or CSS that should be loaded in the rendered HTML document. Check for
@ -561,6 +573,11 @@ export enum ContentScriptType {
* plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts) * plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts)
* to see how the data should be structured. * to see how the data should be structured.
* *
* ## Getting the settings from the renderer
*
* You can access your plugin settings from the renderer by calling
* `pluginOptions.settingValue("your-setting-key')`.
*
* ## Posting messages from the content script to your plugin * ## Posting messages from the content script to your plugin
* *
* The application provides the following function to allow executing * The application provides the following function to allow executing

View File

@ -1,5 +1,5 @@
import { PluginStates } from '../reducer'; import { PluginStates } from '../reducer';
import { ContentScriptType, ContentScriptContext, PostMessageHandler } from '../api/types'; import { ContentScriptType, ContentScriptContext, PostMessageHandler, ContentScriptModule } from '../api/types';
import { dirname } from '@joplin/renderer/pathUtils'; import { dirname } from '@joplin/renderer/pathUtils';
import shim from '../../../shim'; import shim from '../../../shim';
import Logger from '@joplin/utils/Logger'; import Logger from '@joplin/utils/Logger';
@ -11,6 +11,7 @@ export interface ExtraContentScript {
id: string; id: string;
module: any; module: any;
assetPath: string; assetPath: string;
pluginId: string;
} }
function postMessageHandler(pluginId: string, scriptType: ContentScriptType, contentScriptId: string): PostMessageHandler { function postMessageHandler(pluginId: string, scriptType: ContentScriptType, contentScriptId: string): PostMessageHandler {
@ -53,14 +54,15 @@ function loadContentScripts(plugins: PluginStates, scriptType: ContentScriptType
postMessage: postMessageHandler(pluginId, scriptType, contentScript.id), postMessage: postMessageHandler(pluginId, scriptType, contentScript.id),
}; };
const loadedModule = module.default(context); const loadedModule = module.default(context) as ContentScriptModule;
if (!loadedModule.plugin && !loadedModule.codeMirrorResources && !loadedModule.codeMirrorOptions) throw new Error(`Content script must export a "plugin" key or a list of CodeMirror assets or define a CodeMirror option: Plugin: ${pluginId}: Script: ${contentScript.id}`); if (!loadedModule.plugin && !(loadedModule as any).codeMirrorResources && !(loadedModule as any).codeMirrorOptions) throw new Error(`Content script must export a "plugin" key or a list of CodeMirror assets or define a CodeMirror option: Plugin: ${pluginId}: Script: ${contentScript.id}`);
output.push({ output.push({
id: contentScript.id, id: contentScript.id,
module: loadedModule, module: loadedModule,
assetPath: dirname(contentScript.path), assetPath: dirname(contentScript.path),
pluginId,
}); });
} catch (error) { } catch (error) {
// This function must not throw as doing so would crash the // This function must not throw as doing so would crash the

View File

@ -7,10 +7,10 @@ import { ItemIdToUrlHandler } from './utils';
import { RenderResult, RenderResultPluginAsset } from './MarkupToHtml'; import { RenderResult, RenderResultPluginAsset } from './MarkupToHtml';
import { Options as NoteStyleOptions } from './noteStyle'; import { Options as NoteStyleOptions } from './noteStyle';
import hljs from './highlight'; import hljs from './highlight';
import * as MarkdownIt from 'markdown-it';
const Entities = require('html-entities').AllHtmlEntities; const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = new Entities().encode; const htmlentities = new Entities().encode;
const MarkdownIt = require('markdown-it');
const md5 = require('md5'); const md5 = require('md5');
export interface RenderOptions { export interface RenderOptions {
@ -32,6 +32,7 @@ export interface RenderOptions {
useCustomPdfViewer?: boolean; useCustomPdfViewer?: boolean;
noteId?: string; noteId?: string;
vendorDir?: string; vendorDir?: string;
settingValue?: (pluginId: string, key: string)=> any;
} }
interface RendererRule { interface RendererRule {
@ -40,6 +41,7 @@ interface RendererRule {
plugin?: any; plugin?: any;
assetPath?: string; assetPath?: string;
assetPathIsAbsolute?: boolean; assetPathIsAbsolute?: boolean;
pluginId?: string;
} }
interface RendererRules { interface RendererRules {
@ -102,6 +104,7 @@ export interface ExtraRendererRule {
id: string; id: string;
module: any; module: any;
assetPath: string; assetPath: string;
pluginId: string;
} }
export interface Options { export interface Options {
@ -233,7 +236,7 @@ export default class MdToHtml {
if (options.extraRendererRules) { if (options.extraRendererRules) {
for (const rule of options.extraRendererRules) { for (const rule of options.extraRendererRules) {
this.loadExtraRendererRule(rule.id, rule.assetPath, rule.module); this.loadExtraRendererRule(rule.id, rule.assetPath, rule.module, rule.pluginId);
} }
} }
@ -268,11 +271,13 @@ export default class MdToHtml {
} }
// `module` is a file that has already been `required()` // `module` is a file that has already been `required()`
public loadExtraRendererRule(id: string, assetPath: string, module: any) { public loadExtraRendererRule(id: string, assetPath: string, module: any, pluginId: string) {
if (this.extraRendererRules_[id]) throw new Error(`A renderer rule with this ID has already been loaded: ${id}`); if (this.extraRendererRules_[id]) throw new Error(`A renderer rule with this ID has already been loaded: ${id}`);
this.extraRendererRules_[id] = { this.extraRendererRules_[id] = {
...module, ...module,
assetPath, assetPath,
pluginId: pluginId,
assetPathIsAbsolute: true, assetPathIsAbsolute: true,
}; };
} }
@ -451,6 +456,7 @@ export default class MdToHtml {
pdfViewerEnabled: this.pluginEnabled('pdfViewer'), pdfViewerEnabled: this.pluginEnabled('pdfViewer'),
contentMaxWidth: 0, contentMaxWidth: 0,
settingValue: (_pluginId: string, _key: string) => { throw new Error('settingValue is not implemented'); },
...options, ...options,
}; };
@ -467,8 +473,11 @@ export default class MdToHtml {
const cachedOutput = this.cachedOutputs_[cacheKey]; const cachedOutput = this.cachedOutputs_[cacheKey];
if (cachedOutput) return cachedOutput; if (cachedOutput) return cachedOutput;
const ruleOptions = { ...options, resourceBaseUrl: this.resourceBaseUrl_, const ruleOptions = {
ResourceModel: this.ResourceModel_ }; ...options,
resourceBaseUrl: this.resourceBaseUrl_,
ResourceModel: this.ResourceModel_,
};
const context: PluginContext = { const context: PluginContext = {
css: {}, css: {},
@ -478,12 +487,12 @@ export default class MdToHtml {
currentLinks: [], currentLinks: [],
}; };
const markdownIt = new MarkdownIt({ const markdownIt: MarkdownIt = new MarkdownIt({
breaks: !this.pluginEnabled('softbreaks'), breaks: !this.pluginEnabled('softbreaks'),
typographer: this.pluginEnabled('typographer'), typographer: this.pluginEnabled('typographer'),
linkify: this.pluginEnabled('linkify'), linkify: this.pluginEnabled('linkify'),
html: true, html: true,
highlight: (str: string, lang: string) => { highlight: (str: string, lang: string, _attrs: any): any => {
let outputCodeHtml = ''; let outputCodeHtml = '';
// The strings includes the last \n that is part of the fence, // The strings includes the last \n that is part of the fence,
@ -567,6 +576,9 @@ export default class MdToHtml {
context: context, context: context,
...ruleOptions, ...ruleOptions,
...(ruleOptions.plugins[key] ? ruleOptions.plugins[key] : {}), ...(ruleOptions.plugins[key] ? ruleOptions.plugins[key] : {}),
settingValue: (key: string) => {
return options.settingValue(rule.pluginId, key);
},
}); });
} }

View File

@ -20,6 +20,7 @@
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"devDependencies": { "devDependencies": {
"@types/jest": "29.5.5", "@types/jest": "29.5.5",
"@types/markdown-it": "13.0.2",
"@types/node": "18.17.19", "@types/node": "18.17.19",
"jest": "29.7.0", "jest": "29.7.0",
"jest-environment-jsdom": "29.7.0", "jest-environment-jsdom": "29.7.0",

View File

@ -6668,6 +6668,7 @@ __metadata:
"@joplin/fork-uslug": ^1.0.11 "@joplin/fork-uslug": ^1.0.11
"@joplin/utils": ~2.13 "@joplin/utils": ~2.13
"@types/jest": 29.5.5 "@types/jest": 29.5.5
"@types/markdown-it": 13.0.2
"@types/node": 18.17.19 "@types/node": 18.17.19
font-awesome-filetypes: 2.1.0 font-awesome-filetypes: 2.1.0
fs-extra: 11.1.1 fs-extra: 11.1.1