How can I refer the plugin settings from contentScript?

I'm developing a plugin that enables embedding PlantUML diagrams in markdown.

I have defined settings like below:

And I have tried to refer the settings from here in contentScript

But it seems that we can't use plugin's API from there.

How can I refer the plugin settings from contentScript?

Send a message to your plugin, get settings in the plugin, send the result back to the contentscript.

Thank you for your reply!

I have defined onMessage() which returns settings and tried to get the settings from contentScript,

But it seems not allowed for markdown-it plugin.

loadContentScripts: context.postMessage is not available to renderer content scripts

Hmm, I don't think that's possible actually. You can post messages from the HTML that the content script generates, but you can't when you instantiate the Markdown-it plugin. Maybe some way to do this should be provided via the context variable.

If I may make a suggestion, it would make sense to use a fence like this to delimit the UML code:

```plantuml
Bob -> Alice : Hello
```

That would be consistent with how it's done for Mermaid, Fountain, etc.

Thank you for the suggestion!
Now I'm using it as fence.

By the way, I want to switch PlantUML server by settings.
Currently I'm using a public PlantUML server. This means that we can't use this plugin with confidential information.

To switch the server, would you consider to give us a way to get settings from content script?

Yes I think we should add this. Calling postMessage at this point would be tricky because it's async, and that function is synchronous. Perhaps it's a matter of allowing to pass variables from the plugins via the context parameter.

1 Like

It's nice idea. I'm looking forward to the feature being implemented.

I do not recommend to provide it directly, because there are too many tool libraries for encapsulating postMessage/onMessage, the most typical one is ChromeLab’s comlink, ref: GitHub - GoogleChromeLabs/comlink: Comlink makes WebWorkers enjoyable.

+1

@uphy I just went down this same path trying to modify your plugin to add the ability to change the PlantUML server.

For a variety of reasons I cannot use the public PlantUML server for work.

In the meantime--I'll just run a custom version of your plugin that hard-codes the change.

@uphy I am overjoyed with this plugin, thanks so much for creating it. I began using it and testing it out pretty thoroughly over the past few days.

I am using the Joplin desktop version with Dropbox sync both on a MacBook Air and on a PC with Ubuntu 20.04. What I describe below occurs equally, exactly the same, on both machines.

I found that it works well, but every now and then (after extended use, especially with standard library examples from the Hitchhikers Guide docs???) it causes Joplin to crash with an error report. This is sporadic, not connected to errors or length (previous short examples which worked fine before sometimes end up with a Joplin error page making a reboot in safe mode. When this happens, the page causing the crash causes it on both machines.

I am using everything out of the box, although I did install a local PlantUML server with docker, I did not see any settings page in order to try everything out with a local server (I have since read other comments touching on this).

I am willing to do extensive testing if when there are updates to the plugin.

Again, thanks so much for the contribution.

I think you're running into this issue: plugin causes a crash · Issue #3 · uphy/joplin-plantuml-plugin · GitHub

There's a patch (Fix plugin crashing joplin (#3) by mablin7 · Pull Request #4 · uphy/joplin-plantuml-plugin · GitHub) that's already been merged to fix. Hopefully we can get a new version released before too much longer.

Hello, I'm facing the same communication issue with my plugin.
Do you have an example of plugin that is using the method you suggested with Comlink?

Thanks

What kind of content script? And what does it do?

I would like to update the jiraIssue plugin I created in order to use content script.

The current version generates markdown that is automatically rendered.
The problem with this approach is that I pollute the note with a lot of text.
With a content script I could hide this inside the html and css and create a nicer output.

I'm currently not able to read the plugin settings from the content script.

The only way I found to communicate between content script and plugin is to use the webviewApi.postMessage. This api is only available in the rendered HTML and I cannot run it without a user input (like the click of a button or link like you did in the contentScript plugin example).

Can you suggest me another approach?

The only way I found to get the settings from the contentScript is to:

  • create a contentScript plugin that does nothing
  • in the html rendered by the contentScript I call the webviewApi sending the content written inside the fence
  • the joplin plugin combines the content with the settings and replays with the new HTML that will replace the HTML that was calling the webviewApi

I had some problem figuring out how to call the webviewApi when the markdownIt page was rendered, but I finally managed to find a solution.

I use the event:

<style onload=" here the javascript to call the webviewApi" ></style>

I'm not sure this will help the plantuml plugin because it needs the settings to be accessed in the contentScript and not in the HTML rendered by the contentScript.

I hope this can help other plugin developers.
Let me know if you need more details.

1 Like

Well, exactly, this seems to be the only way to do it now and it imposes quite a few limitations. I've spent quite a bit of time trying to rework the BibTeX plugin but in the end I had to start digging into Joplin's source code as the plugin API just does not provide enough features at the moment. We'll see how that goes.

Please, let us know if you find any ineresting hack to have access to some internal features

Edit: Never mind! I was responding with a solution to a similar (but quite different) problem I was experiencing.

A workaround is for the content script to call repeatedly postMessage and await the result (because postMessage returns a Promise). The plugin's entrypoint script can then resolve that Promise when it wants to send a message to the content script.

Example

(Taken from the custom CodeMirror .vimrc plugin and modified)

Type definitions
// Type definitions

interface FooMessage {
	kind: 'foo';
}

interface BarMessage {
	kind: 'bar';
	content: string;
}

interface SetReceiveMessageCallback {
	kind: 'set-callback';
}

type ToContentScriptMessage = FooMessage|BarMessage;
type FromContentScriptMessage = FooMessage|SetReceiveMessageCallback;
// Content script:

type PostMessageCallback = (message: FromContentScriptMessage)=>Promise<string|ToContentScriptMessage>;
type OnMessageCallback = (message: ToContentScriptMessage)=>void;

const setupOnMessageCallback = async (
	onMessage: OnMessageCallback, postMessage: PostMessageCallback
) => {
	// Handle messages from index.ts in a loop:
	while (true) {
		const callbackResult = await postMessage({
			kind: 'set-callback',
		});

		// Result should be a ToContentScriptMessage
		if (typeof callbackResult === 'string') {
			throw new Error(`Invalid callback result: ${callbackResult}`);
		}

		onMessage(callbackResult);
	}
};

export default (context: { contentScriptId: string, postMessage: PostMessageCallback }) => {
	return {
		plugin: () => {
			setupOnMessageCallback(message => {
				console.log('Message from index.ts: ', message);
			}, context.postMessage);
		}
	};
};
// index.ts
joplin.plugins.register({
	onStart: async () => {
		const contentScriptId = `some-content-script-id-here`;
		let messageContentScriptCallback: MessageContentScriptCallback|null = null;
		await joplin.contentScripts.onMessage(contentScriptId, (message: FromContentScriptMessage) => {
			if (message.kind === 'set-callback') {
				return new Promise(resolve => {
					// Store resolve as a callback — we can use it to communicate with the contentScript.
					messageContentScriptCallback = resolve;
				});
			}

			// Handle other message types here.
		});
		await joplin.contentScripts.register(
			ContentScriptType.CodeMirrorPlugin,
			contentScriptId,
			'path/to/content-script.js',
		);
		
		...

		// Send a message to the content script
		messageContentScriptCallback({
			kind: 'foo'
		});
	}
});

From the next version, it will be possible to access the plugin settings from the renderer, using the provided options.settingValue() method (which only returns your own plugin settings).

For example:

export default () => { 
	return {
		plugin: (markdownIt, pluginOptions) => {	
			markdownIt.renderer.rules.fence = function(tokens, idx, options, env, self) {
				const mySettingValue = pluginOptions.settingValue('mySetting')
				// ...								
			};
		},
	}
}

Note that for now it's only supported by renderer content script (not CodeMirror ones).

1 Like