feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -0,0 +1,198 @@
|
||||
import { LogMessageCallback, ContentScriptData } from '../../types';
|
||||
import CodeMirrorControl from '../CodeMirrorControl';
|
||||
import codeMirrorRequire from './codeMirrorRequire';
|
||||
|
||||
let pluginScriptIdCounter = 0;
|
||||
let pluginLoaderCounter = 0;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin exports have dynamic structure
|
||||
type OnScriptLoadCallback = (exports: any)=> void;
|
||||
type OnPluginRemovedCallback = ()=> void;
|
||||
|
||||
type PluginLoaderWindow = Window & {
|
||||
__pluginLoaderScriptLoadCallbacks: Record<number, OnScriptLoadCallback>;
|
||||
__pluginLoaderRequireFunctions: Record<number, typeof codeMirrorRequire>;
|
||||
};
|
||||
|
||||
const contentScriptToId = (contentScript: ContentScriptData) => `${contentScript.pluginId}--${contentScript.contentScriptId}`;
|
||||
|
||||
export default class PluginLoader {
|
||||
private pluginScriptsContainer: HTMLElement;
|
||||
private loadedContentScriptIds: string[] = [];
|
||||
private pluginRemovalCallbacks: Record<string, OnPluginRemovedCallback> = {};
|
||||
private pluginLoaderId: number;
|
||||
|
||||
public constructor(private editor: CodeMirrorControl, private logMessage: LogMessageCallback) {
|
||||
this.pluginScriptsContainer = document.createElement('div');
|
||||
this.pluginScriptsContainer.style.display = 'none';
|
||||
|
||||
// For testing
|
||||
this.pluginScriptsContainer.id = 'joplin-plugin-scripts-container';
|
||||
|
||||
document.body.appendChild(this.pluginScriptsContainer);
|
||||
|
||||
// addPlugin works by creating <script> elements with the plugin's content. To pass
|
||||
// information to this <script>, we use global objects:
|
||||
(window as unknown as PluginLoaderWindow).__pluginLoaderScriptLoadCallbacks ??= Object.create(null);
|
||||
(window as unknown as PluginLoaderWindow).__pluginLoaderRequireFunctions ??= Object.create(null);
|
||||
|
||||
this.pluginLoaderId = pluginLoaderCounter++;
|
||||
(window as unknown as PluginLoaderWindow).__pluginLoaderRequireFunctions[this.pluginLoaderId] = codeMirrorRequire;
|
||||
}
|
||||
|
||||
public async setPlugins(contentScripts: ContentScriptData[]) {
|
||||
for (const contentScript of contentScripts) {
|
||||
const id = contentScriptToId(contentScript);
|
||||
if (!this.loadedContentScriptIds.includes(id)) {
|
||||
this.addPlugin(contentScript);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove old plugins
|
||||
const contentScriptIds = contentScripts.map(contentScriptToId);
|
||||
const removedIds = this.loadedContentScriptIds
|
||||
.filter(id => !contentScriptIds.includes(id));
|
||||
|
||||
for (const id of removedIds) {
|
||||
if (id in this.pluginRemovalCallbacks) {
|
||||
this.pluginRemovalCallbacks[id]();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addPlugin(plugin: ContentScriptData) {
|
||||
const onRemoveCallbacks: OnPluginRemovedCallback[] = [];
|
||||
|
||||
this.logMessage(`Loading plugin ${plugin.pluginId}, content script ${plugin.contentScriptId}`);
|
||||
|
||||
const addScript = (onLoad: OnScriptLoadCallback) => {
|
||||
const scriptElement = document.createElement('script');
|
||||
|
||||
onRemoveCallbacks.push(() => {
|
||||
scriptElement.remove();
|
||||
});
|
||||
|
||||
void (async () => {
|
||||
const scriptId = pluginScriptIdCounter++;
|
||||
const js = await plugin.contentScriptJs({
|
||||
contentScriptStartJs: `
|
||||
(async () => {
|
||||
const exports = {};
|
||||
const module = { exports: exports };
|
||||
const require = window.__pluginLoaderRequireFunctions[${JSON.stringify(this.pluginLoaderId)}];
|
||||
const joplin = {
|
||||
require,
|
||||
};
|
||||
`,
|
||||
contentScriptEndJs: `
|
||||
window.__pluginLoaderScriptLoadCallbacks[${JSON.stringify(scriptId)}](module.exports);
|
||||
})();
|
||||
`,
|
||||
});
|
||||
|
||||
// Stop if cancelled
|
||||
if (!this.loadedContentScriptIds.includes(contentScriptToId(plugin))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (js.sourceJs) {
|
||||
scriptElement.appendChild(document.createTextNode(js.sourceJs));
|
||||
} else {
|
||||
scriptElement.src = js.uri;
|
||||
}
|
||||
|
||||
(window as unknown as PluginLoaderWindow).__pluginLoaderScriptLoadCallbacks[scriptId] = onLoad;
|
||||
|
||||
this.pluginScriptsContainer.appendChild(scriptElement);
|
||||
})();
|
||||
};
|
||||
|
||||
const addStyles = (cssStrings: string[]) => {
|
||||
// A container for style elements
|
||||
const styleContainer = document.createElement('div');
|
||||
|
||||
onRemoveCallbacks.push(() => {
|
||||
styleContainer.remove();
|
||||
});
|
||||
|
||||
for (const cssText of cssStrings) {
|
||||
const style = document.createElement('style');
|
||||
style.innerText = cssText;
|
||||
styleContainer.appendChild(style);
|
||||
}
|
||||
|
||||
this.pluginScriptsContainer.appendChild(styleContainer);
|
||||
};
|
||||
|
||||
this.pluginRemovalCallbacks[contentScriptToId(plugin)] = () => {
|
||||
for (const callback of onRemoveCallbacks) {
|
||||
callback();
|
||||
}
|
||||
|
||||
this.loadedContentScriptIds = this.loadedContentScriptIds.filter(id => {
|
||||
return id !== contentScriptToId(plugin);
|
||||
});
|
||||
};
|
||||
|
||||
addScript(async exports => {
|
||||
if (!exports?.default || !(typeof exports.default === 'function')) {
|
||||
throw new Error('All plugins must have a function default export');
|
||||
}
|
||||
|
||||
const context = {
|
||||
postMessage: plugin.postMessageHandler,
|
||||
pluginId: plugin.pluginId,
|
||||
contentScriptId: plugin.contentScriptId,
|
||||
};
|
||||
const loadedPlugin = exports.default(context) ?? {};
|
||||
|
||||
loadedPlugin.plugin?.(this.editor);
|
||||
|
||||
if (loadedPlugin.codeMirrorOptions) {
|
||||
for (const key in loadedPlugin.codeMirrorOptions) {
|
||||
this.editor.setOption(key, loadedPlugin.codeMirrorOptions[key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (loadedPlugin.assets) {
|
||||
const cssStrings = [];
|
||||
|
||||
for (const asset of loadedPlugin.assets()) {
|
||||
let assetText: string = asset.text;
|
||||
let assetMime: string = asset.mime;
|
||||
|
||||
if (!asset.inline) {
|
||||
if (!asset.name) {
|
||||
throw new Error('Non-inline asset missing required property "name"');
|
||||
}
|
||||
if (assetMime !== 'text/css' && !asset.name.endsWith('.css')) {
|
||||
throw new Error(
|
||||
`Non-css assets are not supported by the CodeMirror 6 editor. (Asset path: ${asset.name})`,
|
||||
);
|
||||
}
|
||||
|
||||
assetText = await plugin.loadCssAsset(asset.name);
|
||||
assetMime = 'text/css';
|
||||
}
|
||||
|
||||
if (assetMime !== 'text/css') {
|
||||
throw new Error(
|
||||
'Plugin assets must have property "mime" set to "text/css" or have a filename ending with ".css"',
|
||||
);
|
||||
}
|
||||
|
||||
cssStrings.push(assetText);
|
||||
}
|
||||
|
||||
addStyles(cssStrings);
|
||||
}
|
||||
});
|
||||
|
||||
this.loadedContentScriptIds.push(contentScriptToId(plugin));
|
||||
}
|
||||
|
||||
public remove() {
|
||||
this.pluginScriptsContainer.remove();
|
||||
}
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户