1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-30 10:36:35 +02:00
joplin/ReactNativeClient/lib/services/plugins/PluginService.ts
Laurent Cozic 3d8577a689 Plugins: Added support for content scripts
- For now, supports Markdown-it plugins
- Also fixed slow rendering of notes in some cases
- Simplified how Markdown-It plugins are created and cleaned MdToHtml code

commit 89576de289
Merge: c75aa21f 5292fc14
Author: Laurent Cozic <laurent@cozic.net>
Date:   Wed Oct 21 00:23:00 2020 +0100

    Merge branch 'release-1.3' into plugin_content_scripts

commit c75aa21ffd
Author: Laurent Cozic <laurent@cozic.net>
Date:   Wed Oct 21 00:19:52 2020 +0100

    Fixed tests

commit 075187729d
Author: Laurent Cozic <laurent@cozic.net>
Date:   Wed Oct 21 00:11:53 2020 +0100

    Fixed tests

commit 14696b8c65
Author: Laurent Cozic <laurent@cozic.net>
Date:   Tue Oct 20 23:27:58 2020 +0100

    Fixed slow rendering of note

commit 61c09f5bf8
Author: Laurent Cozic <laurent@cozic.net>
Date:   Tue Oct 20 22:35:21 2020 +0100

    Clean up

commit 9f7ea7d865
Author: Laurent Cozic <laurent@cozic.net>
Date:   Tue Oct 20 20:05:31 2020 +0100

    Updated doc

commit 98bf3bde8d
Author: Laurent Cozic <laurent@cozic.net>
Date:   Tue Oct 20 19:56:34 2020 +0100

    Finished converting plugins

commit fe90d92e01
Author: Laurent Cozic <laurent@cozic.net>
Date:   Tue Oct 20 17:52:02 2020 +0100

    Simplified how Markdown-It plugins are created

commit 47c7b864cb
Author: Laurent Cozic <laurent@cozic.net>
Date:   Mon Oct 19 16:40:11 2020 +0100

    Clean up rules

commit d927a238bb
Author: Laurent Cozic <laurent@cozic.net>
Date:   Mon Oct 19 14:29:40 2020 +0100

    Fixed tests

commit 388a56c5dd
Author: Laurent Cozic <laurent@cozic.net>
Date:   Mon Oct 19 14:00:47 2020 +0100

    Add support for content scripts
2020-10-21 00:23:55 +01:00

167 lines
4.9 KiB
TypeScript

import Plugin from 'lib/services/plugins/Plugin';
import manifestFromObject from 'lib/services/plugins/utils/manifestFromObject';
import Global from 'lib/services/plugins/api/Global';
import BasePluginRunner from 'lib/services/plugins/BasePluginRunner';
import BaseService from '../BaseService';
import shim from 'lib/shim';
const { filename, dirname } = require('lib/path-utils');
const uslug = require('uslug');
interface Plugins {
[key:string]: Plugin
}
function makePluginId(source:string):string {
// https://www.npmjs.com/package/slug#options
return uslug(source).substr(0,32);
}
export default class PluginService extends BaseService {
private static instance_:PluginService = null;
public static instance():PluginService {
if (!this.instance_) {
this.instance_ = new PluginService();
}
return this.instance_;
}
private store_:any = null;
private platformImplementation_:any = null;
private plugins_:Plugins = {};
private runner_:BasePluginRunner = null;
initialize(platformImplementation:any, runner:BasePluginRunner, store:any) {
this.store_ = store;
this.runner_ = runner;
this.platformImplementation_ = platformImplementation;
}
public get plugins():Plugins {
return this.plugins_;
}
public pluginById(id:string):Plugin {
if (!this.plugins_[id]) throw new Error(`Plugin not found: ${id}`);
return this.plugins_[id];
}
private async parsePluginJsBundle(jsBundleString:string) {
const scriptText = jsBundleString;
const lines = scriptText.split('\n');
const manifestText:string[] = [];
const StateStarted = 1;
const StateInManifest = 2;
let state:number = StateStarted;
for (let line of lines) {
line = line.trim();
if (state !== StateInManifest) {
if (line === '/* joplin-manifest:') {
state = StateInManifest;
}
continue;
}
if (state === StateInManifest) {
if (line.indexOf('*/') === 0) {
break;
} else {
manifestText.push(line);
}
}
}
if (!manifestText.length) throw new Error('Could not find manifest');
return {
scriptText: scriptText,
manifestText: manifestText.join('\n'),
};
}
public async loadPluginFromString(pluginId:string, baseDir:string, jsBundleString:string):Promise<Plugin> {
const r = await this.parsePluginJsBundle(jsBundleString);
return this.loadPlugin(pluginId, baseDir, r.manifestText, r.scriptText);
}
private async loadPluginFromPath(path:string):Promise<Plugin> {
const fsDriver = shim.fsDriver();
if (path.toLowerCase().endsWith('.js')) return this.loadPluginFromString(filename(path), dirname(path), await fsDriver.readFile(path));
let distPath = path;
if (!(await fsDriver.exists(`${distPath}/manifest.json`))) {
distPath = `${path}/dist`;
}
this.logger().info(`PluginService: Loading plugin from ${path}`);
const scriptText = await fsDriver.readFile(`${distPath}/index.js`);
const manifestText = await fsDriver.readFile(`${distPath}/manifest.json`);
const pluginId = makePluginId(filename(path));
return this.loadPlugin(pluginId, distPath, manifestText, scriptText);
}
private async loadPlugin(pluginId:string, baseDir:string, manifestText:string, scriptText:string):Promise<Plugin> {
const manifest = manifestFromObject(JSON.parse(manifestText));
// After transforming the plugin path to an ID, multiple plugins might end up with the same ID. For
// example "MyPlugin" and "myplugin" would have the same ID. Technically it's possible to have two
// such folders but to keep things sane we disallow it.
if (this.plugins_[pluginId]) throw new Error(`There is already a plugin with this ID: ${pluginId}`);
const plugin = new Plugin(pluginId, baseDir, manifest, scriptText, this.logger(), (action:any) => this.store_.dispatch(action));
this.store_.dispatch({
type: 'PLUGIN_ADD',
plugin: {
id: pluginId,
views: {},
contentScripts: {},
},
});
return plugin;
}
public async loadAndRunPlugins(pluginDirOrPaths:string | string[]) {
let pluginPaths = [];
if (Array.isArray(pluginDirOrPaths)) {
pluginPaths = pluginDirOrPaths;
} else {
pluginPaths = (await shim.fsDriver().readDirStats(pluginDirOrPaths))
.filter((stat:any) => (stat.isDirectory() || stat.path.toLowerCase().endsWith('.js')))
.map((stat:any) => `${pluginDirOrPaths}/${stat.path}`);
}
for (const pluginPath of pluginPaths) {
if (pluginPath.indexOf('_') === 0) {
this.logger().info(`PluginService: Plugin name starts with "_" and has not been loaded: ${pluginPath}`);
continue;
}
try {
const plugin = await this.loadPluginFromPath(pluginPath);
await this.runPlugin(plugin);
} catch (error) {
this.logger().error(`PluginService: Could not load plugin: ${pluginPath}`, error);
}
}
}
public async runPlugin(plugin:Plugin) {
this.plugins_[plugin.id] = plugin;
const pluginApi = new Global(this.logger(), this.platformImplementation_, plugin, this.store_);
return this.runner_.run(plugin, pluginApi);
}
}