You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-16 00:14:34 +02:00
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ForwardedRef } from 'react';
|
import { ForwardedRef } from 'react';
|
||||||
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||||
import { EditorProps, LogMessageCallback, OnEventCallback, PluginData } from '@joplin/editor/types';
|
import { EditorProps, LogMessageCallback, OnEventCallback, ContentScriptData } from '@joplin/editor/types';
|
||||||
import createEditor from '@joplin/editor/CodeMirror/createEditor';
|
import createEditor from '@joplin/editor/CodeMirror/createEditor';
|
||||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||||
@ -57,13 +57,13 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugins: PluginData[] = [];
|
const contentScripts: ContentScriptData[] = [];
|
||||||
for (const pluginId in props.pluginStates) {
|
for (const pluginId in props.pluginStates) {
|
||||||
const pluginState = props.pluginStates[pluginId];
|
const pluginState = props.pluginStates[pluginId];
|
||||||
const codeMirrorContentScripts = pluginState.contentScripts[ContentScriptType.CodeMirrorPlugin] ?? [];
|
const codeMirrorContentScripts = pluginState.contentScripts[ContentScriptType.CodeMirrorPlugin] ?? [];
|
||||||
|
|
||||||
for (const contentScript of codeMirrorContentScripts) {
|
for (const contentScript of codeMirrorContentScripts) {
|
||||||
plugins.push({
|
contentScripts.push({
|
||||||
pluginId,
|
pluginId,
|
||||||
contentScriptId: contentScript.id,
|
contentScriptId: contentScript.id,
|
||||||
contentScriptJs: () => shim.fsDriver().readFile(contentScript.path),
|
contentScriptJs: () => shim.fsDriver().readFile(contentScript.path),
|
||||||
@ -80,7 +80,7 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void editor.setPlugins(plugins);
|
void editor.setContentScripts(contentScripts);
|
||||||
}, [editor, props.pluginStates]);
|
}, [editor, props.pluginStates]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -15,7 +15,7 @@ import { EditorControl, EditorSettings, SelectionRange } from './types';
|
|||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import MarkdownToolbar from './MarkdownToolbar/MarkdownToolbar';
|
import MarkdownToolbar from './MarkdownToolbar/MarkdownToolbar';
|
||||||
import { ChangeEvent, EditorEvent, EditorEventType, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
|
import { ChangeEvent, EditorEvent, EditorEventType, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
|
||||||
import { EditorCommandType, EditorKeymap, EditorLanguageType, PluginData, SearchState } from '@joplin/editor/types';
|
import { EditorCommandType, EditorKeymap, EditorLanguageType, ContentScriptData, SearchState } from '@joplin/editor/types';
|
||||||
import supportsCommand from '@joplin/editor/CodeMirror/editorCommands/supportsCommand';
|
import supportsCommand from '@joplin/editor/CodeMirror/editorCommands/supportsCommand';
|
||||||
import SelectionFormatting, { defaultSelectionFormatting } from '@joplin/editor/SelectionFormatting';
|
import SelectionFormatting, { defaultSelectionFormatting } from '@joplin/editor/SelectionFormatting';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
@ -240,8 +240,8 @@ const useEditorControl = (
|
|||||||
injectJS('document.activeElement?.blur();');
|
injectJS('document.activeElement?.blur();');
|
||||||
},
|
},
|
||||||
|
|
||||||
setPlugins: async (plugins: PluginData[]) => {
|
setContentScripts: async (plugins: ContentScriptData[]) => {
|
||||||
injectJS(`cm.setPlugins(${JSON.stringify(plugins)});`);
|
injectJS(`cm.setContentScripts(${JSON.stringify(plugins)});`);
|
||||||
},
|
},
|
||||||
|
|
||||||
setSearchState: setSearchStateCallback,
|
setSearchState: setSearchStateCallback,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { EditorView } from '@codemirror/view';
|
import { EditorView } from '@codemirror/view';
|
||||||
import { EditorCommandType, EditorControl, EditorSettings, LogMessageCallback, PluginData, SearchState } from '../types';
|
import { EditorCommandType, EditorControl, EditorSettings, LogMessageCallback, ContentScriptData, SearchState } from '../types';
|
||||||
import CodeMirror5Emulation from './CodeMirror5Emulation/CodeMirror5Emulation';
|
import CodeMirror5Emulation from './CodeMirror5Emulation/CodeMirror5Emulation';
|
||||||
import editorCommands, { EditorCommandFunction } from './editorCommands/editorCommands';
|
import editorCommands, { EditorCommandFunction } from './editorCommands/editorCommands';
|
||||||
import { EditorSelection, Extension, StateEffect } from '@codemirror/state';
|
import { EditorSelection, Extension, StateEffect } from '@codemirror/state';
|
||||||
@ -136,7 +136,7 @@ export default class CodeMirrorControl extends CodeMirror5Emulation implements E
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public setPlugins(plugins: PluginData[]) {
|
public setContentScripts(plugins: ContentScriptData[]) {
|
||||||
return this._pluginControl.setPlugins(plugins);
|
return this._pluginControl.setPlugins(plugins);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,10 @@ import createEditorSettings from './testUtil/createEditorSettings';
|
|||||||
describe('createEditor', () => {
|
describe('createEditor', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
for (const scriptContainer of document.querySelectorAll('#joplin-plugin-scripts-container')) {
|
||||||
|
scriptContainer.remove();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// This checks for a regression -- occasionally, when updating packages,
|
// This checks for a regression -- occasionally, when updating packages,
|
||||||
@ -89,7 +93,7 @@ describe('createEditor', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Should be able to load a plugin
|
// Should be able to load a plugin
|
||||||
await editor.setPlugins([
|
await editor.setContentScripts([
|
||||||
testPlugin1,
|
testPlugin1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -99,14 +103,14 @@ describe('createEditor', () => {
|
|||||||
// Because plugin loading is done by adding script elements to the document,
|
// Because plugin loading is done by adding script elements to the document,
|
||||||
// we test for the presence of these script elements, rather than waiting for
|
// we test for the presence of these script elements, rather than waiting for
|
||||||
// them to run.
|
// them to run.
|
||||||
expect(document.querySelectorAll('#joplin-plugin-scripts-container')).toHaveLength(1);
|
expect(document.querySelectorAll('#joplin-plugin-scripts-container script')).toHaveLength(1);
|
||||||
|
|
||||||
// Only one script should be present.
|
// Only one script should be present.
|
||||||
const scriptContainer = document.querySelector('#joplin-plugin-scripts-container');
|
const scriptContainer = document.querySelector('#joplin-plugin-scripts-container');
|
||||||
expect(scriptContainer.querySelectorAll('script')).toHaveLength(1);
|
expect(scriptContainer.querySelectorAll('script')).toHaveLength(1);
|
||||||
|
|
||||||
// Adding another plugin should add another script element
|
// Adding another plugin should add another script element
|
||||||
await editor.setPlugins([
|
await editor.setContentScripts([
|
||||||
testPlugin2, testPlugin1,
|
testPlugin2, testPlugin1,
|
||||||
]);
|
]);
|
||||||
await jest.runAllTimersAsync();
|
await jest.runAllTimersAsync();
|
||||||
@ -116,6 +120,53 @@ describe('createEditor', () => {
|
|||||||
|
|
||||||
// Removing the editor should remove the script container
|
// Removing the editor should remove the script container
|
||||||
editor.remove();
|
editor.remove();
|
||||||
expect(document.querySelectorAll('#joplin-plugin-scripts-container')).toHaveLength(0);
|
expect(document.querySelectorAll('#joplin-plugin-scripts-container script')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support multiple content scripts from the same plugin', async () => {
|
||||||
|
const initialText = '# Test\nThis is a test.';
|
||||||
|
const editorSettings = createEditorSettings(Setting.THEME_LIGHT);
|
||||||
|
|
||||||
|
const editor = createEditor(document.body, {
|
||||||
|
initialText,
|
||||||
|
settings: editorSettings,
|
||||||
|
onEvent: _event => {},
|
||||||
|
onLogMessage: _message => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getContentScriptJs = jest.fn(async () => {
|
||||||
|
return `
|
||||||
|
exports.default = context => {
|
||||||
|
context.postMessage(context.pluginId);
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
const postMessageHandler = jest.fn();
|
||||||
|
|
||||||
|
const pluginId = 'a.plugin.id';
|
||||||
|
const testPlugin1 = {
|
||||||
|
pluginId,
|
||||||
|
contentScriptId: 'a.plugin.id.contentScript',
|
||||||
|
loadCssAsset: async (_name: string) => '',
|
||||||
|
contentScriptJs: getContentScriptJs,
|
||||||
|
postMessageHandler,
|
||||||
|
};
|
||||||
|
const testPlugin2 = {
|
||||||
|
pluginId,
|
||||||
|
contentScriptId: 'another.plugin.id.contentScript',
|
||||||
|
loadCssAsset: async (_name: string) => '',
|
||||||
|
contentScriptJs: getContentScriptJs,
|
||||||
|
postMessageHandler,
|
||||||
|
};
|
||||||
|
|
||||||
|
await editor.setContentScripts([
|
||||||
|
testPlugin1, testPlugin2,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Allows plugins to load
|
||||||
|
await jest.runAllTimersAsync();
|
||||||
|
|
||||||
|
// Should be one script container for each plugin
|
||||||
|
expect(document.querySelectorAll('#joplin-plugin-scripts-container script')).toHaveLength(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { LogMessageCallback, PluginData } from '../../types';
|
import { LogMessageCallback, ContentScriptData } from '../../types';
|
||||||
import CodeMirrorControl from '../CodeMirrorControl';
|
import CodeMirrorControl from '../CodeMirrorControl';
|
||||||
import codeMirrorRequire from './codeMirrorRequire';
|
import codeMirrorRequire from './codeMirrorRequire';
|
||||||
|
|
||||||
@ -8,9 +8,11 @@ let pluginLoaderCounter = 0;
|
|||||||
type OnScriptLoadCallback = (exports: any)=> void;
|
type OnScriptLoadCallback = (exports: any)=> void;
|
||||||
type OnPluginRemovedCallback = ()=> void;
|
type OnPluginRemovedCallback = ()=> void;
|
||||||
|
|
||||||
|
const contentScriptToId = (contentScript: ContentScriptData) => `${contentScript.pluginId}--${contentScript.contentScriptId}`;
|
||||||
|
|
||||||
export default class PluginLoader {
|
export default class PluginLoader {
|
||||||
private pluginScriptsContainer: HTMLElement;
|
private pluginScriptsContainer: HTMLElement;
|
||||||
private loadedPluginIds: string[] = [];
|
private loadedContentScriptIds: string[] = [];
|
||||||
private pluginRemovalCallbacks: Record<string, OnPluginRemovedCallback> = {};
|
private pluginRemovalCallbacks: Record<string, OnPluginRemovedCallback> = {};
|
||||||
private pluginLoaderId: number;
|
private pluginLoaderId: number;
|
||||||
|
|
||||||
@ -32,17 +34,18 @@ export default class PluginLoader {
|
|||||||
(window as any).__pluginLoaderRequireFunctions[this.pluginLoaderId] = codeMirrorRequire;
|
(window as any).__pluginLoaderRequireFunctions[this.pluginLoaderId] = codeMirrorRequire;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setPlugins(plugins: PluginData[]) {
|
public async setPlugins(contentScripts: ContentScriptData[]) {
|
||||||
for (const plugin of plugins) {
|
for (const contentScript of contentScripts) {
|
||||||
if (!this.loadedPluginIds.includes(plugin.pluginId)) {
|
const id = contentScriptToId(contentScript);
|
||||||
this.addPlugin(plugin);
|
if (!this.loadedContentScriptIds.includes(id)) {
|
||||||
|
this.addPlugin(contentScript);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove old plugins
|
// Remove old plugins
|
||||||
const pluginIds = plugins.map(plugin => plugin.pluginId);
|
const contentScriptIds = contentScripts.map(contentScriptToId);
|
||||||
const removedIds = this.loadedPluginIds
|
const removedIds = this.loadedContentScriptIds
|
||||||
.filter(id => !pluginIds.includes(id));
|
.filter(id => !contentScriptIds.includes(id));
|
||||||
|
|
||||||
for (const id of removedIds) {
|
for (const id of removedIds) {
|
||||||
if (id in this.pluginRemovalCallbacks) {
|
if (id in this.pluginRemovalCallbacks) {
|
||||||
@ -51,10 +54,10 @@ export default class PluginLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private addPlugin(plugin: PluginData) {
|
private addPlugin(plugin: ContentScriptData) {
|
||||||
const onRemoveCallbacks: OnPluginRemovedCallback[] = [];
|
const onRemoveCallbacks: OnPluginRemovedCallback[] = [];
|
||||||
|
|
||||||
this.logMessage(`Loading plugin ${plugin.pluginId}`);
|
this.logMessage(`Loading plugin ${plugin.pluginId}, content script ${plugin.contentScriptId}`);
|
||||||
|
|
||||||
const addScript = (onLoad: OnScriptLoadCallback) => {
|
const addScript = (onLoad: OnScriptLoadCallback) => {
|
||||||
const scriptElement = document.createElement('script');
|
const scriptElement = document.createElement('script');
|
||||||
@ -68,7 +71,7 @@ export default class PluginLoader {
|
|||||||
const js = await plugin.contentScriptJs();
|
const js = await plugin.contentScriptJs();
|
||||||
|
|
||||||
// Stop if cancelled
|
// Stop if cancelled
|
||||||
if (!this.loadedPluginIds.includes(plugin.pluginId)) {
|
if (!this.loadedContentScriptIds.includes(contentScriptToId(plugin))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,13 +112,13 @@ export default class PluginLoader {
|
|||||||
this.pluginScriptsContainer.appendChild(styleContainer);
|
this.pluginScriptsContainer.appendChild(styleContainer);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.pluginRemovalCallbacks[plugin.pluginId] = () => {
|
this.pluginRemovalCallbacks[contentScriptToId(plugin)] = () => {
|
||||||
for (const callback of onRemoveCallbacks) {
|
for (const callback of onRemoveCallbacks) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadedPluginIds = this.loadedPluginIds.filter(id => {
|
this.loadedContentScriptIds = this.loadedContentScriptIds.filter(id => {
|
||||||
return id !== plugin.pluginId;
|
return id !== contentScriptToId(plugin);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -173,7 +176,7 @@ export default class PluginLoader {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.loadedPluginIds.push(plugin.pluginId);
|
this.loadedContentScriptIds.push(contentScriptToId(plugin));
|
||||||
}
|
}
|
||||||
|
|
||||||
public remove() {
|
public remove() {
|
||||||
|
@ -64,7 +64,7 @@ export enum EditorCommandType {
|
|||||||
|
|
||||||
// Because the editor package can run in a WebView, plugin content scripts
|
// Because the editor package can run in a WebView, plugin content scripts
|
||||||
// need to be provided as text, rather than as file paths.
|
// need to be provided as text, rather than as file paths.
|
||||||
export interface PluginData {
|
export interface ContentScriptData {
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
contentScriptId: string;
|
contentScriptId: string;
|
||||||
contentScriptJs: ()=> Promise<string>;
|
contentScriptJs: ()=> Promise<string>;
|
||||||
@ -95,7 +95,7 @@ export interface EditorControl {
|
|||||||
|
|
||||||
setSearchState(state: SearchState): void;
|
setSearchState(state: SearchState): void;
|
||||||
|
|
||||||
setPlugins(plugins: PluginData[]): Promise<void>;
|
setContentScripts(plugins: ContentScriptData[]): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EditorLanguageType {
|
export enum EditorLanguageType {
|
||||||
|
Reference in New Issue
Block a user