1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-17 18:44:45 +02:00

Plugins: Added support for bi-directional messages in content scripts and webview scripts using postMessage

This commit is contained in:
Laurent Cozic 2021-01-11 23:33:10 +00:00
parent cbad3b1190
commit 2489409abb
23 changed files with 574 additions and 128 deletions

View File

@ -224,6 +224,15 @@ packages/app-cli/tests/support/plugins/multi_selection/api/types.js.map
packages/app-cli/tests/support/plugins/multi_selection/src/index.d.ts packages/app-cli/tests/support/plugins/multi_selection/src/index.d.ts
packages/app-cli/tests/support/plugins/multi_selection/src/index.js packages/app-cli/tests/support/plugins/multi_selection/src/index.js
packages/app-cli/tests/support/plugins/multi_selection/src/index.js.map packages/app-cli/tests/support/plugins/multi_selection/src/index.js.map
packages/app-cli/tests/support/plugins/post_messages/api/index.d.ts
packages/app-cli/tests/support/plugins/post_messages/api/index.js
packages/app-cli/tests/support/plugins/post_messages/api/index.js.map
packages/app-cli/tests/support/plugins/post_messages/api/types.d.ts
packages/app-cli/tests/support/plugins/post_messages/api/types.js
packages/app-cli/tests/support/plugins/post_messages/api/types.js.map
packages/app-cli/tests/support/plugins/post_messages/src/index.d.ts
packages/app-cli/tests/support/plugins/post_messages/src/index.js
packages/app-cli/tests/support/plugins/post_messages/src/index.js.map
packages/app-cli/tests/support/plugins/register_command/api/index.d.ts packages/app-cli/tests/support/plugins/register_command/api/index.d.ts
packages/app-cli/tests/support/plugins/register_command/api/index.js packages/app-cli/tests/support/plugins/register_command/api/index.js
packages/app-cli/tests/support/plugins/register_command/api/index.js.map packages/app-cli/tests/support/plugins/register_command/api/index.js.map
@ -1016,6 +1025,9 @@ packages/lib/services/KeymapService_keysRegExp.js.map
packages/lib/services/KvStore.d.ts packages/lib/services/KvStore.d.ts
packages/lib/services/KvStore.js packages/lib/services/KvStore.js
packages/lib/services/KvStore.js.map packages/lib/services/KvStore.js.map
packages/lib/services/PostMessageService.d.ts
packages/lib/services/PostMessageService.js
packages/lib/services/PostMessageService.js.map
packages/lib/services/ResourceEditWatcher/index.d.ts packages/lib/services/ResourceEditWatcher/index.d.ts
packages/lib/services/ResourceEditWatcher/index.js packages/lib/services/ResourceEditWatcher/index.js
packages/lib/services/ResourceEditWatcher/index.js.map packages/lib/services/ResourceEditWatcher/index.js.map
@ -1154,6 +1166,9 @@ packages/lib/services/plugins/api/Joplin.js.map
packages/lib/services/plugins/api/JoplinCommands.d.ts packages/lib/services/plugins/api/JoplinCommands.d.ts
packages/lib/services/plugins/api/JoplinCommands.js packages/lib/services/plugins/api/JoplinCommands.js
packages/lib/services/plugins/api/JoplinCommands.js.map packages/lib/services/plugins/api/JoplinCommands.js.map
packages/lib/services/plugins/api/JoplinContentScripts.d.ts
packages/lib/services/plugins/api/JoplinContentScripts.js
packages/lib/services/plugins/api/JoplinContentScripts.js.map
packages/lib/services/plugins/api/JoplinData.d.ts packages/lib/services/plugins/api/JoplinData.d.ts
packages/lib/services/plugins/api/JoplinData.js packages/lib/services/plugins/api/JoplinData.js
packages/lib/services/plugins/api/JoplinData.js.map packages/lib/services/plugins/api/JoplinData.js.map

15
.gitignore vendored
View File

@ -213,6 +213,15 @@ packages/app-cli/tests/support/plugins/multi_selection/api/types.js.map
packages/app-cli/tests/support/plugins/multi_selection/src/index.d.ts packages/app-cli/tests/support/plugins/multi_selection/src/index.d.ts
packages/app-cli/tests/support/plugins/multi_selection/src/index.js packages/app-cli/tests/support/plugins/multi_selection/src/index.js
packages/app-cli/tests/support/plugins/multi_selection/src/index.js.map packages/app-cli/tests/support/plugins/multi_selection/src/index.js.map
packages/app-cli/tests/support/plugins/post_messages/api/index.d.ts
packages/app-cli/tests/support/plugins/post_messages/api/index.js
packages/app-cli/tests/support/plugins/post_messages/api/index.js.map
packages/app-cli/tests/support/plugins/post_messages/api/types.d.ts
packages/app-cli/tests/support/plugins/post_messages/api/types.js
packages/app-cli/tests/support/plugins/post_messages/api/types.js.map
packages/app-cli/tests/support/plugins/post_messages/src/index.d.ts
packages/app-cli/tests/support/plugins/post_messages/src/index.js
packages/app-cli/tests/support/plugins/post_messages/src/index.js.map
packages/app-cli/tests/support/plugins/register_command/api/index.d.ts packages/app-cli/tests/support/plugins/register_command/api/index.d.ts
packages/app-cli/tests/support/plugins/register_command/api/index.js packages/app-cli/tests/support/plugins/register_command/api/index.js
packages/app-cli/tests/support/plugins/register_command/api/index.js.map packages/app-cli/tests/support/plugins/register_command/api/index.js.map
@ -1005,6 +1014,9 @@ packages/lib/services/KeymapService_keysRegExp.js.map
packages/lib/services/KvStore.d.ts packages/lib/services/KvStore.d.ts
packages/lib/services/KvStore.js packages/lib/services/KvStore.js
packages/lib/services/KvStore.js.map packages/lib/services/KvStore.js.map
packages/lib/services/PostMessageService.d.ts
packages/lib/services/PostMessageService.js
packages/lib/services/PostMessageService.js.map
packages/lib/services/ResourceEditWatcher/index.d.ts packages/lib/services/ResourceEditWatcher/index.d.ts
packages/lib/services/ResourceEditWatcher/index.js packages/lib/services/ResourceEditWatcher/index.js
packages/lib/services/ResourceEditWatcher/index.js.map packages/lib/services/ResourceEditWatcher/index.js.map
@ -1143,6 +1155,9 @@ packages/lib/services/plugins/api/Joplin.js.map
packages/lib/services/plugins/api/JoplinCommands.d.ts packages/lib/services/plugins/api/JoplinCommands.d.ts
packages/lib/services/plugins/api/JoplinCommands.js packages/lib/services/plugins/api/JoplinCommands.js
packages/lib/services/plugins/api/JoplinCommands.js.map packages/lib/services/plugins/api/JoplinCommands.js.map
packages/lib/services/plugins/api/JoplinContentScripts.d.ts
packages/lib/services/plugins/api/JoplinContentScripts.js
packages/lib/services/plugins/api/JoplinContentScripts.js.map
packages/lib/services/plugins/api/JoplinData.d.ts packages/lib/services/plugins/api/JoplinData.d.ts
packages/lib/services/plugins/api/JoplinData.js packages/lib/services/plugins/api/JoplinData.js
packages/lib/services/plugins/api/JoplinData.js.map packages/lib/services/plugins/api/JoplinData.js.map

View File

@ -192,7 +192,7 @@ describe('services_PluginService', function() {
joplin.plugins.register({ joplin.plugins.register({
onStart: async function() { onStart: async function() {
await joplin.plugins.registerContentScript('markdownItPlugin', 'justtesting', './markdownItTestPlugin.js'); await joplin.contentScripts.register('markdownItPlugin', 'justtesting', './markdownItTestPlugin.js');
}, },
}); });
`); `);

View File

@ -8,7 +8,6 @@ import NoteEditor from '../NoteEditor/NoteEditor';
import NoteContentPropertiesDialog from '../NoteContentPropertiesDialog'; import NoteContentPropertiesDialog from '../NoteContentPropertiesDialog';
import ShareNoteDialog from '../ShareNoteDialog'; import ShareNoteDialog from '../ShareNoteDialog';
import CommandService from '@joplin/lib/services/CommandService'; import CommandService from '@joplin/lib/services/CommandService';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer'; import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import SideBar from '../SideBar/SideBar'; import SideBar from '../SideBar/SideBar';
import UserWebview from '../../services/plugins/UserWebview'; import UserWebview from '../../services/plugins/UserWebview';
@ -30,7 +29,6 @@ import { themeStyle } from '@joplin/lib/theme';
import validateLayout from '../ResizableLayout/utils/validateLayout'; import validateLayout from '../ResizableLayout/utils/validateLayout';
import iterateItems from '../ResizableLayout/utils/iterateItems'; import iterateItems from '../ResizableLayout/utils/iterateItems';
import removeItem from '../ResizableLayout/utils/removeItem'; import removeItem from '../ResizableLayout/utils/removeItem';
import Logger from '@joplin/lib/Logger';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { PromptDialog } = require('../PromptDialog.min.js'); const { PromptDialog } = require('../PromptDialog.min.js');
@ -39,8 +37,6 @@ const PluginManager = require('@joplin/lib/services/PluginManager');
const EncryptionService = require('@joplin/lib/services/EncryptionService'); const EncryptionService = require('@joplin/lib/services/EncryptionService');
const ipcRenderer = require('electron').ipcRenderer; const ipcRenderer = require('electron').ipcRenderer;
const logger = Logger.create('MainScreen');
interface LayerModalState { interface LayerModalState {
visible: boolean; visible: boolean;
message: string; message: string;
@ -157,7 +153,6 @@ class MainScreenComponent extends React.Component<Props, State> {
this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this); this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this);
this.noteContentPropertiesDialog_close = this.noteContentPropertiesDialog_close.bind(this); this.noteContentPropertiesDialog_close = this.noteContentPropertiesDialog_close.bind(this);
this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this); this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this);
this.userWebview_message = this.userWebview_message.bind(this);
this.resizableLayout_resize = this.resizableLayout_resize.bind(this); this.resizableLayout_resize = this.resizableLayout_resize.bind(this);
this.resizableLayout_renderItem = this.resizableLayout_renderItem.bind(this); this.resizableLayout_renderItem = this.resizableLayout_renderItem.bind(this);
this.resizableLayout_moveButtonClick = this.resizableLayout_moveButtonClick.bind(this); this.resizableLayout_moveButtonClick = this.resizableLayout_moveButtonClick.bind(this);
@ -567,11 +562,6 @@ class MainScreenComponent extends React.Component<Props, State> {
} }
} }
userWebview_message(event: any) {
logger.debug('Got message (WebView => Plugin) (2)', event);
PluginService.instance().pluginById(event.pluginId).viewController(event.viewId).emitMessage(event);
}
resizableLayout_resize(event: any) { resizableLayout_resize(event: any) {
this.updateMainLayout(event.layout); this.updateMainLayout(event.layout);
} }
@ -584,6 +574,8 @@ class MainScreenComponent extends React.Component<Props, State> {
resizableLayout_renderItem(key: string, event: any) { resizableLayout_renderItem(key: string, event: any) {
const eventEmitter = event.eventEmitter; const eventEmitter = event.eventEmitter;
// const viewsToRemove:string[] = [];
const components: any = { const components: any = {
sideBar: () => { sideBar: () => {
return <SideBar key={key} />; return <SideBar key={key} />;
@ -612,26 +604,46 @@ class MainScreenComponent extends React.Component<Props, State> {
const viewInfo = pluginUtils.viewInfoByViewId(this.props.plugins, event.item.key); const viewInfo = pluginUtils.viewInfoByViewId(this.props.plugins, event.item.key);
if (!viewInfo) { if (!viewInfo) {
// Note that it will happen when the component is rendered
// before the plugins have loaded their views, so because of
// this we need to keep the view in the layout.
//
// But it can also be a problem if the view really is invalid
// due to a faulty plugin as currently there would be no way to
// remove it.
console.warn(`Could not find plugin associated with view: ${event.item.key}`); console.warn(`Could not find plugin associated with view: ${event.item.key}`);
return null; return null;
} else {
const { view, plugin } = viewInfo;
return <UserWebview
key={view.id}
viewId={view.id}
themeId={this.props.themeId}
html={view.html}
scripts={view.scripts}
pluginId={plugin.id}
borderBottom={true}
fitToContent={false}
/>;
} }
} else {
const { view, plugin } = viewInfo; throw new Error(`Invalid layout component: ${key}`);
return <UserWebview
key={view.id}
viewId={view.id}
themeId={this.props.themeId}
html={view.html}
scripts={view.scripts}
pluginId={plugin.id}
onMessage={this.userWebview_message}
borderBottom={true}
fitToContent={false}
/>;
} }
throw new Error(`Invalid layout component: ${key}`); // if (viewsToRemove.length) {
// window.requestAnimationFrame(() => {
// let newLayout = this.props.mainLayout;
// for (const itemKey of viewsToRemove) {
// newLayout = removeItem(newLayout, itemKey);
// }
// if (newLayout !== this.props.mainLayout) {
// console.warn('Removed invalid views:', viewsToRemove);
// this.updateMainLayout(newLayout);
// }
// });
// }
} }
renderPluginDialogs() { renderPluginDialogs() {
@ -650,7 +662,6 @@ class MainScreenComponent extends React.Component<Props, State> {
html={view.html} html={view.html}
scripts={view.scripts} scripts={view.scripts}
pluginId={plugin.id} pluginId={plugin.id}
onMessage={this.userWebview_message}
buttons={view.buttons} buttons={view.buttons}
/>); />);
} }

View File

@ -4,6 +4,7 @@ import contextMenu from './contextMenu';
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index'; import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import CommandService from '@joplin/lib/services/CommandService'; import CommandService from '@joplin/lib/services/CommandService';
import PostMessageService from '@joplin/lib/services/PostMessageService';
const BaseItem = require('@joplin/lib/models/BaseItem'); const BaseItem = require('@joplin/lib/models/BaseItem');
const BaseModel = require('@joplin/lib/BaseModel').default; const BaseModel = require('@joplin/lib/BaseModel').default;
const Resource = require('@joplin/lib/models/Resource.js'); const Resource = require('@joplin/lib/models/Resource.js');
@ -95,6 +96,8 @@ export default function useMessageHandler(scrollWhenReady: any, setScrollWhenRea
const commandName = arg0.name; const commandName = arg0.name;
const commandArgs = arg0.args || []; const commandArgs = arg0.args || [];
void CommandService.instance().execute(commandName, ...commandArgs); void CommandService.instance().execute(commandName, ...commandArgs);
} else if (msg === 'postMessageService.message') {
void PostMessageService.instance().postMessage(arg0);
} else { } else {
bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg)); bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));
} }

View File

@ -1,3 +1,4 @@
import PostMessageService, { MessageResponse, ResponderComponentType } from '@joplin/lib/services/PostMessageService';
import * as React from 'react'; import * as React from 'react';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { reg } = require('@joplin/lib/registry.js'); const { reg } = require('@joplin/lib/registry.js');
@ -20,6 +21,19 @@ class NoteTextViewerComponent extends React.Component<Props, any> {
this.webviewRef_ = React.createRef(); this.webviewRef_ = React.createRef();
PostMessageService.instance().registerResponder(ResponderComponentType.NoteTextViewer, '', (message: MessageResponse) => {
if (!this.webviewRef_?.current?.contentWindow) {
reg.logger().warn('Cannot respond to message because target is gone', message);
return;
}
this.webviewRef_.current.contentWindow.postMessage({
target: 'webview',
name: 'postMessageService.response',
data: message,
}, '*');
});
this.webview_domReady = this.webview_domReady.bind(this); this.webview_domReady = this.webview_domReady.bind(this);
this.webview_ipcMessage = this.webview_ipcMessage.bind(this); this.webview_ipcMessage = this.webview_ipcMessage.bind(this);
this.webview_load = this.webview_load.bind(this); this.webview_load = this.webview_load.bind(this);

View File

@ -55,16 +55,30 @@
window.postMessage({ target: 'main', name: methodName, args: [ arg ] }, '*'); window.postMessage({ target: 'main', name: methodName, args: [ arg ] }, '*');
} }
const webviewApiPromises_ = {};
// This function is reserved for plugin, currently only to allow // This function is reserved for plugin, currently only to allow
// executing a command, but more features could be added to the object // executing a command, but more features could be added to the object
// later on. // later on.
const webviewApi = { const webviewApi = {
executeCommand: function (commandName, ...args) { postMessage: function(contentScriptId, message) {
return ipcProxySendToHost('contentScriptExecuteCommand', { const messageId = 'noteViewer_' + Date.now() + Math.random();
name: commandName,
args: args, const promise = new Promise((resolve, reject) => {
webviewApiPromises_[messageId] = { resolve, reject };
}); });
}
ipcProxySendToHost('postMessageService.message', {
contentScriptId: contentScriptId,
viewId: '',
from: 'contentScript',
to: 'plugin',
id: messageId,
content: message,
});
return promise;
},
} }
let pluginAssetsAdded_ = {}; let pluginAssetsAdded_ = {};
@ -75,7 +89,7 @@
const ipc = {}; const ipc = {};
window.addEventListener('message', webviewLib.logEnabledEventHandler(event => { window.addEventListener('message', webviewLib.logEnabledEventHandler(event => {
// Here we only deal with messages that are sent from the main Electro process to the webview. // Here we only deal with messages that are sent from the main Electron process to the webview.
if (!event.data || event.data.target !== 'webview') return; if (!event.data || event.data.target !== 'webview') return;
const callName = event.data.name; const callName = event.data.name;
@ -367,6 +381,20 @@
ipcProxySendToHost('percentScroll', percent); ipcProxySendToHost('percentScroll', percent);
})); }));
ipc['postMessageService.response'] = function(event) {
const promise = webviewApiPromises_[event.responseId];
if (!promise) {
console.warn('postMessageService.response: could not find callback for message', event);
return;
}
if (event.error) {
promise.reject(event.error);
} else {
promise.resolve(event.response);
}
}
document.addEventListener('contextmenu', webviewLib.logEnabledEventHandler(event => { document.addEventListener('contextmenu', webviewLib.logEnabledEventHandler(event => {
let element = event.target; let element = event.target;

View File

@ -15,7 +15,6 @@ const logger = Logger.create('UserWebview');
export interface Props { export interface Props {
html: string; html: string;
scripts: string[]; scripts: string[];
onMessage: Function;
pluginId: string; pluginId: string;
viewId: string; viewId: string;
themeId: number; themeId: number;
@ -119,9 +118,9 @@ function UserWebview(props: Props, ref: any) {
useWebviewToPluginMessages( useWebviewToPluginMessages(
frameWindow(), frameWindow(),
isReady, isReady,
props.onMessage,
props.pluginId, props.pluginId,
props.viewId props.viewId,
postMessage
); );
useScriptLoader( useScriptLoader(

View File

@ -102,7 +102,6 @@ export default function UserWebviewDialog(props: Props) {
ref={webviewRef} ref={webviewRef}
html={props.html} html={props.html}
scripts={props.scripts} scripts={props.scripts}
onMessage={props.onMessage}
pluginId={props.pluginId} pluginId={props.pluginId}
viewId={props.viewId} viewId={props.viewId}
themeId={props.themeId} themeId={props.themeId}

View File

@ -1,8 +1,26 @@
// This is the API that JS files loaded from the webview can see // This is the API that JS files loaded from the webview can see
const webviewApiPromises_ = {};
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const webviewApi = { const webviewApi = {
postMessage: function(message) { postMessage: function(message) {
window.postMessage({ target: 'plugin', message: message }, '*'); const messageId = `userWebview_${Date.now()}${Math.random()}`;
const promise = new Promise((resolve, reject) => {
webviewApiPromises_[messageId] = { resolve, reject };
});
window.postMessage({
target: 'postMessageService.message',
message: {
from: 'userWebview',
to: 'plugin',
id: messageId,
content: message,
},
});
return promise;
}, },
}; };
@ -94,6 +112,21 @@ const webviewApi = {
addScript(scriptPath); addScript(scriptPath);
} }
}, },
'postMessageService.response': (event) => {
const message = event.message;
const promise = webviewApiPromises_[message.responseId];
if (!promise) {
console.warn('postMessageService.response: could not find callback for message', message);
return;
}
if (message.error) {
promise.reject(message.error);
} else {
promise.resolve(message.response);
}
},
}; };
window.addEventListener('message', ((event) => { window.addEventListener('message', ((event) => {

View File

@ -1,24 +1,27 @@
import Logger from '@joplin/lib/Logger'; import PostMessageService, { MessageResponse, ResponderComponentType } from '@joplin/lib/services/PostMessageService';
import { useEffect } from 'react'; import { useEffect } from 'react';
const logger = Logger.create('useWebviewToPluginMessages'); export default function(frameWindow: any, isReady: boolean, pluginId: string, viewId: string, postMessage: Function) {
useEffect(() => {
PostMessageService.instance().registerResponder(ResponderComponentType.UserWebview, viewId, (message: MessageResponse) => {
postMessage('postMessageService.response', { message });
});
return () => {
PostMessageService.instance().unregisterResponder(ResponderComponentType.UserWebview, viewId);
};
}, [viewId]);
export default function(frameWindow: any, isReady: boolean, onMessage: Function, pluginId: string, viewId: string) {
useEffect(() => { useEffect(() => {
if (!frameWindow) return () => {}; if (!frameWindow) return () => {};
function onMessage_(event: any) { function onMessage_(event: any) {
if (!event.data || event.data.target !== 'plugin') return; if (!event.data || event.data.target !== 'postMessageService.message') return;
// The message is passed from one component or service to the next void PostMessageService.instance().postMessage({
// till it reaches its destination, so if something doesn't work pluginId,
// follow the chain of messages searching for the string "Got message" viewId,
logger.debug('Got message (WebView => Plugin) (1)', pluginId, viewId, event.data.message); ...event.data.message,
onMessage({
pluginId: pluginId,
viewId: viewId,
message: event.data.message,
}); });
} }
@ -27,5 +30,5 @@ export default function(frameWindow: any, isReady: boolean, onMessage: Function,
return () => { return () => {
frameWindow.removeEventListener('message', onMessage_); frameWindow.removeEventListener('message', onMessage_);
}; };
}, [frameWindow, onMessage, isReady, pluginId, viewId]); }, [frameWindow, isReady, pluginId, viewId]);
} }

View File

@ -4,5 +4,5 @@
# It could be used to develop plugins too. # It could be used to develop plugins too.
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
PLUGIN_PATH="$SCRIPT_DIR/../app-cli/tests/support/plugins/toc" PLUGIN_PATH="$SCRIPT_DIR/../app-cli/tests/support/plugins/post_messages"
npm i --prefix="$PLUGIN_PATH" && npm start -- --dev-plugins "$PLUGIN_PATH" npm i --prefix="$PLUGIN_PATH" && npm start -- --dev-plugins "$PLUGIN_PATH"

View File

@ -0,0 +1,133 @@
// Passing messages across the various sandbox boundaries can be complex and is
// hard to unit test. This class is an attempt to clarify and track what happens
// when messages are sent.
//
// Essentially it works like this:
//
// The component that might post messages, for example from a content script to
// the plugin, and expect responses:
//
// - First it registers a responder with the PostMessageService - this is what
// will be used to send back responses.
// - Whenever it sends a message it calls PostMessageService.postMessage() and
// wait for the response
// - This class forwards the message to the relevant participant and wait for the
// response
// - Then it sends back the response to the component using the registered
// responder.
//
// There's still quite a bit of boiler plate code on the content script or
// webview side to mask the complexity of passing messages. In particular, it
// needs to create and return a promise when a message is posted. Then in
// another location, when the response is received, it resolves that promise.
// See UserWebviewIndex.js to see how it's done.
import Logger from '../Logger';
import PluginService from './plugins/PluginService';
const logger = Logger.create('PostMessageService');
enum MessageParticipant {
ContentScript = 'contentScript',
Plugin = 'plugin',
UserWebview = 'userWebview',
}
export enum ResponderComponentType {
NoteTextViewer = 'noteTextViewer',
UserWebview = 'userWebview',
}
export interface MessageResponse {
responseId: string;
response: any;
error: any;
}
type MessageResponder = (message: MessageResponse)=> void;
interface Message {
pluginId: string;
contentScriptId: string;
viewId: string;
from: MessageParticipant;
to: MessageParticipant;
id: string;
content: any;
}
export default class PostMessageService {
private static instance_: PostMessageService;
private responders_: Record<string, MessageResponder> = {};
public static instance(): PostMessageService {
if (this.instance_) return this.instance_;
this.instance_ = new PostMessageService();
return this.instance_;
}
public async postMessage(message: Message) {
logger.debug('postMessage:', message);
let response = null;
let error = null;
try {
if (message.from === MessageParticipant.ContentScript && message.to === MessageParticipant.Plugin) {
const pluginId = PluginService.instance().pluginIdByContentScriptId(message.contentScriptId);
if (!pluginId) throw new Error(`Could not find plugin associated with content script "${message.contentScriptId}"`);
response = await PluginService.instance().pluginById(pluginId).emitContentScriptMessage(message.contentScriptId, message.content);
} else if (message.from === MessageParticipant.UserWebview && message.to === MessageParticipant.Plugin) {
response = await PluginService.instance().pluginById(message.pluginId).viewController(message.viewId).emitMessage({ message: message.content });
} else {
throw new Error(`Unhandled message: ${JSON.stringify(message)}`);
}
} catch (e) {
error = e;
}
this.sendResponse(message, response, error);
}
private sendResponse(message: Message, responseContent: any, error: any) {
logger.debug('sendResponse', message, responseContent, error);
let responder: MessageResponder = null;
if (message.from === MessageParticipant.ContentScript) {
responder = this.responder(ResponderComponentType.NoteTextViewer, message.viewId);
} else if (message.from === MessageParticipant.UserWebview) {
responder = this.responder(ResponderComponentType.UserWebview, message.viewId);
}
if (!responder) {
logger.warn('Cannot respond to message because no responder was found', message);
}
responder({
responseId: message.id,
response: responseContent,
error,
});
}
private responder(type: ResponderComponentType, viewId: string): any {
return this.responders_[[type, viewId].join(':')];
}
public registerResponder(type: ResponderComponentType, viewId: string, responder: MessageResponder) {
this.responders_[[type, viewId].join(':')] = responder;
}
public unregisterResponder(type: ResponderComponentType, viewId: string) {
delete this.responders_[[type, viewId].join(':')];
}
}

View File

@ -31,6 +31,8 @@ export default class Plugin {
private dispatch_: Function; private dispatch_: Function;
private eventEmitter_: any; private eventEmitter_: any;
private devMode_: boolean = false; private devMode_: boolean = false;
private messageListener_: Function = null;
private contentScriptMessageListeners_: Record<string, Function> = {};
constructor(baseDir: string, manifest: PluginManifest, scriptText: string, dispatch: Function) { constructor(baseDir: string, manifest: PluginManifest, scriptText: string, dispatch: Function) {
this.baseDir_ = shim.fsDriver().resolve(baseDir); this.baseDir_ = shim.fsDriver().resolve(baseDir);
@ -106,6 +108,17 @@ export default class Plugin {
return this.contentScripts_[type] ? this.contentScripts_[type] : []; return this.contentScripts_[type] ? this.contentScripts_[type] : [];
} }
public contentScriptById(id: string): ContentScript {
for (const type in this.contentScripts_) {
const cs = this.contentScripts_[type];
for (const c of cs) {
if (c.id === id) return c;
}
}
return null;
}
public addViewController(v: ViewController) { public addViewController(v: ViewController) {
if (this.viewControllers_[v.handle]) throw new Error(`View already added or there is already a view with this ID: ${v.handle}`); if (this.viewControllers_[v.handle]) throw new Error(`View already added or there is already a view with this ID: ${v.handle}`);
this.viewControllers_[v.handle] = v; this.viewControllers_[v.handle] = v;
@ -120,4 +133,29 @@ export default class Plugin {
logger.warn(`"${this.id}": DEPRECATION NOTICE: ${message} This will stop working in version ${goneInVersion}.`); logger.warn(`"${this.id}": DEPRECATION NOTICE: ${message} This will stop working in version ${goneInVersion}.`);
} }
public emitMessage(message: any) {
if (!this.messageListener_) return;
return this.messageListener_(message);
}
public onMessage(callback: any) {
this.messageListener_ = callback;
}
public onContentScriptMessage(id: string, callback: any) {
if (!this.contentScriptById(id)) {
// The script could potentially be registered later on, but still
// best to print a warning to notify the user of a possible bug.
logger.warn(`onContentScriptMessage: No such content script: ${id}`);
}
this.contentScriptMessageListeners_[id] = callback;
}
public emitContentScriptMessage(id: string, message: any) {
if (!this.contentScriptMessageListeners_[id]) return;
return this.contentScriptMessageListeners_[id](message);
}
} }

View File

@ -67,7 +67,7 @@ export default class PluginService extends BaseService {
private plugins_: Plugins = {}; private plugins_: Plugins = {};
private runner_: BasePluginRunner = null; private runner_: BasePluginRunner = null;
initialize(appVersion: string, platformImplementation: any, runner: BasePluginRunner, store: any) { public initialize(appVersion: string, platformImplementation: any, runner: BasePluginRunner, store: any) {
this.appVersion_ = appVersion; this.appVersion_ = appVersion;
this.store_ = store; this.store_ = store;
this.runner_ = runner; this.runner_ = runner;
@ -115,6 +115,15 @@ export default class PluginService extends BaseService {
return JSON.stringify(settings); return JSON.stringify(settings);
} }
public pluginIdByContentScriptId(contentScriptId: string): string {
for (const pluginId in this.plugins_) {
const plugin = this.plugins_[pluginId];
const contentScript = plugin.contentScriptById(contentScriptId);
if (contentScript) return pluginId;
}
return null;
}
private async parsePluginJsBundle(jsBundleString: string) { private async parsePluginJsBundle(jsBundleString: string) {
const scriptText = jsBundleString; const scriptText = jsBundleString;
const lines = scriptText.split('\n'); const lines = scriptText.split('\n');

View File

@ -1,5 +1,9 @@
import { ViewHandle } from './utils/createViewHandle'; import { ViewHandle } from './utils/createViewHandle';
export interface EmitMessageEvent {
message: any;
}
export default class ViewController { export default class ViewController {
private handle_: ViewHandle; private handle_: ViewHandle;
@ -36,7 +40,7 @@ export default class ViewController {
throw new Error('Must be overriden'); throw new Error('Must be overriden');
} }
public emitMessage(event: any) { public async emitMessage(event: EmitMessageEvent): Promise<any> {
console.info('Calling ViewController.emitMessage - but not implemented', event); console.info('Calling ViewController.emitMessage - but not implemented', event);
} }

View File

@ -1,4 +1,4 @@
import ViewController from './ViewController'; import ViewController, { EmitMessageEvent } from './ViewController';
import shim from '../../shim'; import shim from '../../shim';
import { ButtonSpec, DialogResult } from './api/types'; import { ButtonSpec, DialogResult } from './api/types';
const { toSystemSlashes } = require('../../path-utils'); const { toSystemSlashes } = require('../../path-utils');
@ -102,9 +102,9 @@ export default class WebviewController extends ViewController {
}); });
} }
public emitMessage(event: any) { public async emitMessage(event: EmitMessageEvent): Promise<any> {
if (!this.messageListener_) return; if (!this.messageListener_) return;
this.messageListener_(event.message); return this.messageListener_(event.message);
} }
public onMessage(callback: any) { public onMessage(callback: any) {

View File

@ -7,6 +7,7 @@ import JoplinCommands from './JoplinCommands';
import JoplinViews from './JoplinViews'; import JoplinViews from './JoplinViews';
import JoplinInterop from './JoplinInterop'; import JoplinInterop from './JoplinInterop';
import JoplinSettings from './JoplinSettings'; import JoplinSettings from './JoplinSettings';
import JoplinContentScripts from './JoplinContentScripts';
/** /**
* This is the main entry point to the Joplin API. You can access various services using the provided accessors. * This is the main entry point to the Joplin API. You can access various services using the provided accessors.
@ -33,6 +34,7 @@ export default class Joplin {
private views_: JoplinViews = null; private views_: JoplinViews = null;
private interop_: JoplinInterop = null; private interop_: JoplinInterop = null;
private settings_: JoplinSettings = null; private settings_: JoplinSettings = null;
private contentScripts_: JoplinContentScripts = null;
constructor(implementation: any, plugin: Plugin, store: any) { constructor(implementation: any, plugin: Plugin, store: any) {
this.data_ = new JoplinData(); this.data_ = new JoplinData();
@ -43,6 +45,7 @@ export default class Joplin {
this.views_ = new JoplinViews(implementation.views, plugin, store); this.views_ = new JoplinViews(implementation.views, plugin, store);
this.interop_ = new JoplinInterop(); this.interop_ = new JoplinInterop();
this.settings_ = new JoplinSettings(plugin); this.settings_ = new JoplinSettings(plugin);
this.contentScripts_ = new JoplinContentScripts(plugin);
} }
get data(): JoplinData { get data(): JoplinData {
@ -57,6 +60,10 @@ export default class Joplin {
return this.workspace_; return this.workspace_;
} }
get contentScripts(): JoplinContentScripts {
return this.contentScripts_;
}
/** /**
* @ignore * @ignore
* *

View File

@ -0,0 +1,52 @@
import Plugin from '../Plugin';
import { ContentScriptType } from './types';
export default class JoplinContentScripts {
private plugin: Plugin;
public constructor(plugin: Plugin) {
this.plugin = plugin;
}
/**
* 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.
*
* The plugin generator provides a way to build any content script you might
* want to package as well as its dependencies. See the [Plugin Generator
* doc](https://github.com/laurent22/joplin/blob/dev/packages/generator-joplin/README.md)
* for more information.
*
* * [View the renderer demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
* * [View the editor demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script)
*
* See also the [postMessage demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages)
*
* @param type Defines how the script will be used. See the type definition for more information about each supported type.
* @param id A unique ID for the 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`.
*/
public async register(type: ContentScriptType, id: string, scriptPath: string) {
return this.plugin.registerContentScript(type, id, scriptPath);
}
/**
* Listens to a messages sent from the content script using postMessage().
* See {@link ContentScriptType} for more information as well as the
* [postMessage
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages)
*/
public async onMessage(contentScriptId: string, callback: any) {
this.plugin.onContentScriptMessage(contentScriptId, callback);
}
}

View File

@ -51,30 +51,19 @@ export default class JoplinPlugins {
} }
/** /**
* Registers a new content script. Unlike regular plugin code, which runs in * @deprecated Use joplin.contentScripts.register()
* 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.
*
* The plugin generator provides a way to build any content script you might
* want to package as well as its dependencies. See the [Plugin Generator
* doc](https://github.com/laurent22/joplin/blob/dev/packages/generator-joplin/README.md)
* for more information.
*
* * [View the renderer demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
* * [View the editor demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script)
*
* @param type Defines how the script will be used. See the type definition for more information about each supported type.
* @param id A unique ID for the 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`.
*/ */
public async registerContentScript(type: ContentScriptType, id: string, scriptPath: string) { public async registerContentScript(type: ContentScriptType, id: string, scriptPath: string) {
this.plugin.deprecationNotice('1.8', 'joplin.plugins.registerContentScript() is deprecated in favour of joplin.contentScripts.register()');
return this.plugin.registerContentScript(type, id, scriptPath); return this.plugin.registerContentScript(type, id, scriptPath);
} }
// public async onMessage(callback: any) {
// this.plugin.onMessage(callback);
// }
// public async onContentScriptMessage(id: string, callback: any) {
// this.plugin.onContentScriptMessage(id, callback);
// }
} }

View File

@ -57,6 +57,22 @@ export default class JoplinViewsPanels {
/** /**
* Called when a message is sent from the webview (using postMessage). * Called when a message is sent from the webview (using postMessage).
*
* To post a message from the webview to the plugin use:
*
* ```javascript
* const response = await webviewApi.postMessage(message);
* ```
*
* - `message` can be any JavaScript object, string or number
* - `response` is whatever was returned by the `onMessage` handler
*
* Using this mechanism, you can have two-way communication between the
* plugin and webview.
*
* See the [postMessage
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) for more details.
*
*/ */
public async onMessage(handle: ViewHandle, callback: Function) { public async onMessage(handle: ViewHandle, callback: Function) {
return this.controller(handle).onMessage(callback); return this.controller(handle).onMessage(callback);

View File

@ -366,9 +366,31 @@ export interface SettingSection {
export type Path = string[]; export type Path = string[];
// ================================================================= // =================================================================
// Plugins type // Content Script types
// ================================================================= // =================================================================
export type PostMessageHandler = (id: string, message: any)=> Promise<any>;
/**
* When a content script is initialised, it receives a `context` object.
*/
export interface ContentScriptContext {
/**
* The plugin ID that registered this content script
*/
pluginId: string;
/**
* The content script ID, which may be necessary to post messages
*/
contentScriptId: string;
/**
* Can be used by CodeMirror content scripts to post a message to the plugin
*/
postMessage: PostMessageHandler;
}
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
@ -394,43 +416,56 @@ export enum ContentScriptType {
* *
* ## Exported members * ## Exported members
* *
* - The `context` argument is currently unused but could be used later * - The `context` argument is currently unused but could be used later on
* on to provide access to your own plugin so that the content script * to provide access to your own plugin so that the content script and
* and plugin can communicate. * plugin can communicate.
* *
* - The **required** `plugin` key is the actual Markdown-It plugin - * - The **required** `plugin` key is the actual Markdown-It plugin - check
* check the [official * the [official doc](https://github.com/markdown-it/markdown-it) for more
* doc](https://github.com/markdown-it/markdown-it) for more
* information. The `options` parameter is of type * information. The `options` parameter is of type
* [RuleOptions](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml.ts), * [RuleOptions](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml.ts),
* which contains a number of options, mostly useful for Joplin's * which contains a number of options, mostly useful for Joplin's internal
* internal code. * code.
* *
* - Using the **optional** `assets` key you may specify assets such as * - Using the **optional** `assets` key you may specify assets such as JS
* JS or CSS that should be loaded in the rendered HTML document. * or CSS that should be loaded in the rendered HTML document. Check for
* Check for example the Joplin [Mermaid * example the Joplin [Mermaid
* 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.
* *
* ## Passing 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
* commands from the rendered HTML code: * commands from the rendered HTML code:
* *
* `webviewApi.executeCommand(commandName, ...args)` * ```javascript
* const response = await webviewApi.postMessage(contentScriptId, message);
* ```
* *
* So you can use this mechanism to pass messages from the note viewer * - `contentScriptId` is the ID you've defined when you registered the
* to your own plugin. To do so you would define a command, using * content script. You can retrieve it from the
* `joplin.commands.register`, then you would call this command using * {@link ContentScriptContext | context}.
* the `webviewApi` object. See again [the * - `message` can be any basic JavaScript type (number, string, plain
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) * object), but it cannot be a function or class instance.
* to see how this can be done. *
* When you post a message, the plugin can send back a `response` thus
* allowing two-way communication:
*
* ```javascript
* await joplin.contentScripts.onMessage(contentScriptId, (message) => {
* // Process message
* return response; // Can be any object, string or number
* });
* ```
*
* See {@link JoplinContentScripts.onMessage} for more details, as well as
* the [postMessage
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages).
* *
* ## Registering an existing Markdown-it plugin * ## Registering an existing Markdown-it plugin
* *
* To include a regular Markdown-It plugin, that doesn't make use of * To include a regular Markdown-It plugin, that doesn't make use of any
* any Joplin-specific features, you would simply create a file such as * Joplin-specific features, you would simply create a file such as this:
* this:
* *
* ```javascript * ```javascript
* module.exports = { * module.exports = {
@ -443,6 +478,7 @@ export enum ContentScriptType {
* ``` * ```
*/ */
MarkdownItPlugin = 'markdownItPlugin', MarkdownItPlugin = 'markdownItPlugin',
/** /**
* Registers a new CodeMirror plugin, which should follow the template * Registers a new CodeMirror plugin, which should follow the template
* below. * below.
@ -466,42 +502,65 @@ export enum ContentScriptType {
* } * }
* ``` * ```
* *
* - The `context` argument is currently unused but could be used later * - The `context` argument is currently unused but could be used later on
* on to provide access to your own plugin so that the content script * to provide access to your own plugin so that the content script and
* and plugin can communicate. * plugin can communicate.
* *
* - The `plugin` key is your CodeMirror plugin. This is where you can * - The `plugin` key is your CodeMirror plugin. This is where you can
* register new commands with CodeMirror or interact with the * register new commands with CodeMirror or interact with the CodeMirror
* CodeMirror instance as needed. * instance as needed.
* *
* - The `codeMirrorResources` key is an array of CodeMirror resources * - The `codeMirrorResources` key is an array of CodeMirror resources that
* that will be loaded and attached to the CodeMirror module. These * will be loaded and attached to the CodeMirror module. These are made up
* are made up of addons, keymaps, and modes. For example, for a * of addons, keymaps, and modes. For example, for a plugin that want's to
* plugin that want's to enable clojure highlighting in code blocks. * enable clojure highlighting in code blocks. `codeMirrorResources` would
* `codeMirrorResources` would be set to `['mode/clojure/clojure']`. * be set to `['mode/clojure/clojure']`.
* *
* - The `codeMirrorOptions` key contains all the * - The `codeMirrorOptions` key contains all the
* [CodeMirror](https://codemirror.net/doc/manual.html#config) * [CodeMirror](https://codemirror.net/doc/manual.html#config) options
* options that will be set or changed by this plugin. New options * that will be set or changed by this plugin. New options can alse be
* can alse be declared via * declared via
* [`CodeMirror.defineOption`](https://codemirror.net/doc/manual.html#defineOption), * [`CodeMirror.defineOption`](https://codemirror.net/doc/manual.html#defineOption),
* and then have their value set here. For example, a plugin that * and then have their value set here. For example, a plugin that enables
* enables line numbers would set `codeMirrorOptions` to * line numbers would set `codeMirrorOptions` to `{'lineNumbers': true}`.
* `{'lineNumbers': true}`.
* *
* - Using the **optional** `assets` key you may specify **only** CSS * - Using the **optional** `assets` key you may specify **only** CSS assets
* assets that should be loaded in the rendered HTML document. Check * that should be loaded in the rendered HTML document. Check for example
* for example the Joplin [Mermaid * the Joplin [Mermaid
* 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.
* *
* One of the `plugin`, `codeMirrorResources`, or `codeMirrorOptions` * One of the `plugin`, `codeMirrorResources`, or `codeMirrorOptions` keys
* keys must be provided for the plugin to be valid. Having multiple or * must be provided for the plugin to be valid. Having multiple or all
* all provided is also okay. * provided is also okay.
* *
* See the [demo * See also the [demo
* plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script) * plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script)
* for an example of all these keys being used in one plugin. * for an example of all these keys being used in one plugin.
*
* ## Posting messages from the content script to your plugin
*
* In order to post messages to the plugin, you can use the postMessage
* function passed to the {@link ContentScriptContext | context}.
*
* ```javascript
* const response = await context.postMessage('messageFromCodeMirrorContentScript');
* ```
*
* When you post a message, the plugin can send back a `response` thus
* allowing two-way communication:
*
* ```javascript
* await joplin.contentScripts.onMessage(contentScriptId, (message) => {
* // Process message
* return response; // Can be any object, string or number
* });
* ```
*
* See {@link JoplinContentScripts.onMessage} for more details, as well as
* the [postMessage
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages).
*
*/ */
CodeMirrorPlugin = 'codeMirrorPlugin', CodeMirrorPlugin = 'codeMirrorPlugin',
} }

View File

@ -1,8 +1,9 @@
import { PluginStates } from '../reducer'; import { PluginStates } from '../reducer';
import { ContentScriptType } from '../api/types'; import { ContentScriptType, ContentScriptContext, PostMessageHandler } 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 '../../../Logger'; import Logger from '../../../Logger';
import PluginService from '../PluginService';
const logger = Logger.create('loadContentScripts'); const logger = Logger.create('loadContentScripts');
@ -12,6 +13,17 @@ export interface ExtraContentScript {
assetPath: string; assetPath: string;
} }
function postMessageHandler(pluginId: string, scriptType: ContentScriptType, contentScriptId: string): PostMessageHandler {
return (message: any) => {
if (scriptType === ContentScriptType.MarkdownItPlugin) {
logger.error('context.postMessage is not available to renderer content scripts');
} else {
const plugin = PluginService.instance().pluginById(pluginId);
return plugin.emitContentScriptMessage(contentScriptId, message);
}
};
}
export function contentScriptsToRendererRules(plugins: PluginStates): ExtraContentScript[] { export function contentScriptsToRendererRules(plugins: PluginStates): ExtraContentScript[] {
return loadContentScripts(plugins, ContentScriptType.MarkdownItPlugin); return loadContentScripts(plugins, ContentScriptType.MarkdownItPlugin);
} }
@ -35,7 +47,14 @@ function loadContentScripts(plugins: PluginStates, scriptType: ContentScriptType
const module = shim.requireDynamic(contentScript.path); const module = shim.requireDynamic(contentScript.path);
if (!module.default || typeof module.default !== 'function') throw new Error(`Content script must export a function under the "default" key: Plugin: ${pluginId}: Script: ${contentScript.id}`); if (!module.default || typeof module.default !== 'function') throw new Error(`Content script must export a function under the "default" key: Plugin: ${pluginId}: Script: ${contentScript.id}`);
const loadedModule = module.default({}); const context: ContentScriptContext = {
pluginId,
contentScriptId: contentScript.id,
postMessage: postMessageHandler(pluginId, contentScript.id),
};
const loadedModule = module.default(context);
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.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}`);
output.push({ output.push({