UNPKG

zotero-plugin-toolkit

Version:
4,393 lines 131 kB
import { __export } from "./chunk-Cl8Af3a2.js";

//#region package.json
var version = "5.1.0-beta.8";

//#endregion
//#region src/utils/debugBridge.ts
/**
* Debug bridge.
*
* @deprecated Since this is a temporary solution for debugging, it is not recommended to use.
* The zotero-plugin-scaffold no longer need this.
*
* @remarks
* Global variables: Zotero, window(main window).
*
* @example
* Run script directly. The `run` is URIencoded script.
*
* `zotero://ztoolkit-debug/?run=Zotero.getMainWindow().alert(%22HelloWorld!%22)&app=developer`
* @example
* Run script from file. The `file` is URIencoded path to js file starts with `file:///`
*
* `zotero://ztoolkit-debug/?file=file%3A%2F%2F%2FC%3A%2FUsers%2Fw_xia%2FDesktop%2Frun.js&app=developer`
*/
var DebugBridge = class DebugBridge {
	static version = 2;
	static passwordPref = "extensions.zotero.debug-bridge.password";
	get version() {
		return DebugBridge.version;
	}
	_disableDebugBridgePassword;
	get disableDebugBridgePassword() {
		return this._disableDebugBridgePassword;
	}
	set disableDebugBridgePassword(value) {
		this._disableDebugBridgePassword = value;
	}
	get password() {
		return BasicTool.getZotero().Prefs.get(DebugBridge.passwordPref, true);
	}
	set password(v) {
		BasicTool.getZotero().Prefs.set(DebugBridge.passwordPref, v, true);
	}
	constructor() {
		this._disableDebugBridgePassword = false;
		this.initializeDebugBridge();
	}
	static setModule(instance) {
		if (!instance.debugBridge?.version || instance.debugBridge.version < DebugBridge.version) instance.debugBridge = new DebugBridge();
	}
	initializeDebugBridge() {
		const debugBridgeExtension = {
			noContent: true,
			doAction: async (uri) => {
				const Zotero$1 = BasicTool.getZotero();
				const window$1 = Zotero$1.getMainWindow();
				const uriString = uri.spec.split("//").pop();
				if (!uriString) return;
				const params = {};
				uriString.split("?").pop()?.split("&").forEach((p) => {
					params[p.split("=")[0]] = decodeURIComponent(p.split("=")[1]);
				});
				const skipPasswordCheck = toolkitGlobal_default.getInstance()?.debugBridge.disableDebugBridgePassword;
				let allowed = false;
				if (skipPasswordCheck) allowed = true;
				else if (typeof params.password === "undefined" && typeof this.password === "undefined") allowed = window$1.confirm(`External App ${params.app} wants to execute command without password.\nCommand:\n${(params.run || params.file || "").slice(0, 100)}\nIf you do not know what it is, please click Cancel to deny.`);
				else allowed = this.password === params.password;
				if (allowed) {
					if (params.run) try {
						const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
						const f = new AsyncFunction("Zotero,window", params.run);
						await f(Zotero$1, window$1);
					} catch (e) {
						Zotero$1.debug(e);
						window$1.console.log(e);
					}
					if (params.file) try {
						Services.scriptloader.loadSubScript(params.file, {
							Zotero: Zotero$1,
							window: window$1
						});
					} catch (e) {
						Zotero$1.debug(e);
						window$1.console.log(e);
					}
				}
			},
			newChannel(uri) {
				this.doAction(uri);
			}
		};
		Services.io.getProtocolHandler("zotero").wrappedJSObject._extensions["zotero://ztoolkit-debug"] = debugBridgeExtension;
	}
};

//#endregion
//#region src/utils/pluginBridge.ts
/**
* Plugin bridge. Install plugin from zotero://plugin
*
* @deprecated Since this is a temporary solution for debugging, it is not recommended to use.
* The zotero-plugin-scaffold no longer need this.
*
* @example
* Install plugin from url, with minimal Zotero version requirement.
* ```text
* zotero://plugin/?action=install&url=https%3A%2F%2Fgithub.com%2FMuiseDestiny%2Fzotero-style%2Freleases%2Fdownload%2F3.0.5%2Fzotero-style.xpi&minVersion=6.999
* ```
*/
var PluginBridge = class PluginBridge {
	static version = 1;
	get version() {
		return PluginBridge.version;
	}
	constructor() {
		this.initializePluginBridge();
	}
	static setModule(instance) {
		if (!instance.pluginBridge?.version || instance.pluginBridge.version < PluginBridge.version) instance.pluginBridge = new PluginBridge();
	}
	initializePluginBridge() {
		const { AddonManager } = _importESModule("resource://gre/modules/AddonManager.sys.mjs");
		const Zotero$1 = BasicTool.getZotero();
		const pluginBridgeExtension = {
			noContent: true,
			doAction: async (uri) => {
				try {
					const uriString = uri.spec.split("//").pop();
					if (!uriString) return;
					const params = {};
					uriString.split("?").pop()?.split("&").forEach((p) => {
						params[p.split("=")[0]] = decodeURIComponent(p.split("=")[1]);
					});
					if (params.action === "install" && params.url) {
						if (params.minVersion && Services.vc.compare(Zotero$1.version, params.minVersion) < 0 || params.maxVersion && Services.vc.compare(Zotero$1.version, params.maxVersion) > 0) throw new Error(`Plugin is not compatible with Zotero version ${Zotero$1.version}.The plugin requires Zotero version between ${params.minVersion} and ${params.maxVersion}.`);
						const addon = await AddonManager.getInstallForURL(params.url);
						if (addon && addon.state === AddonManager.STATE_AVAILABLE) {
							addon.install();
							hint("Plugin installed successfully.", true);
						} else throw new Error(`Plugin ${params.url} is not available.`);
					}
				} catch (e) {
					Zotero$1.logError(e);
					hint(e.message, false);
				}
			},
			newChannel(uri) {
				this.doAction(uri);
			}
		};
		Services.io.getProtocolHandler("zotero").wrappedJSObject._extensions["zotero://plugin"] = pluginBridgeExtension;
	}
};
function hint(content, success) {
	const progressWindow = new Zotero.ProgressWindow({ closeOnClick: true });
	progressWindow.changeHeadline("Plugin Toolkit");
	progressWindow.progress = new progressWindow.ItemProgress(success ? "chrome://zotero/skin/tick.png" : "chrome://zotero/skin/cross.png", content);
	progressWindow.progress.setProgress(100);
	progressWindow.show();
	progressWindow.startCloseTimer(5e3);
}

//#endregion
//#region src/managers/toolkitGlobal.ts
/**
* The Singleton class of global parameters used by managers.
* @example `ToolkitGlobal.getInstance().itemTree.state`
*/
var ToolkitGlobal = class ToolkitGlobal {
	debugBridge;
	pluginBridge;
	prompt;
	currentWindow;
	constructor() {
		initializeModules(this);
		this.currentWindow = BasicTool.getZotero().getMainWindow();
	}
	/**
	* Get the global unique instance of `class ToolkitGlobal`.
	* @returns An instance of `ToolkitGlobal`.
	*/
	static getInstance() {
		let _Zotero;
		try {
			if (typeof Zotero !== "undefined") _Zotero = Zotero;
			else _Zotero = BasicTool.getZotero();
		} catch {}
		if (!_Zotero) return void 0;
		let requireInit = false;
		if (!("_toolkitGlobal" in _Zotero)) {
			_Zotero._toolkitGlobal = new ToolkitGlobal();
			requireInit = true;
		}
		const currentGlobal = _Zotero._toolkitGlobal;
		if (currentGlobal.currentWindow !== _Zotero.getMainWindow()) {
			checkWindowDependentModules(currentGlobal);
			requireInit = true;
		}
		if (requireInit) initializeModules(currentGlobal);
		return currentGlobal;
	}
};
/**
* Initialize global modules using the data of this toolkit build.
* Modules and their properties that do not exist will be updated.
* @param instance ToolkitGlobal instance
*/
function initializeModules(instance) {
	new BasicTool().log("Initializing ToolkitGlobal modules");
	setModule(instance, "prompt", {
		_ready: false,
		instance: void 0
	});
	DebugBridge.setModule(instance);
	PluginBridge.setModule(instance);
}
function setModule(instance, key, module) {
	if (!module) return;
	if (!instance[key]) instance[key] = module;
	for (const moduleKey in module) instance[key][moduleKey] ??= module[moduleKey];
}
function checkWindowDependentModules(instance) {
	instance.currentWindow = BasicTool.getZotero().getMainWindow();
	instance.prompt = void 0;
}
var toolkitGlobal_default = ToolkitGlobal;

//#endregion
//#region src/basic.ts
/**
* Basic APIs with Zotero 6 & newer (7) compatibility.
* See also https://www.zotero.org/support/dev/zotero_7_for_developers
*/
var BasicTool = class BasicTool {
	/**
	* configurations.
	*/
	_basicOptions;
	_console;
	/**
	* @deprecated Use `patcherManager` instead.
	*/
	patchSign = "zotero-plugin-toolkit@3.0.0";
	static _version = version;
	/**
	* Get version - checks subclass first, then falls back to parent
	*/
	get _version() {
		return version;
	}
	get basicOptions() {
		return this._basicOptions;
	}
	/**
	*
	* @param data Pass an BasicTool instance to copy its options.
	*/
	constructor(data) {
		this._basicOptions = {
			log: {
				_type: "toolkitlog",
				disableConsole: false,
				disableZLog: false,
				prefix: ""
			},
			get debug() {
				if (this._debug) return this._debug;
				this._debug = toolkitGlobal_default.getInstance()?.debugBridge || {
					disableDebugBridgePassword: false,
					password: ""
				};
				return this._debug;
			},
			api: { pluginID: "zotero-plugin-toolkit@windingwind.com" },
			listeners: {
				callbacks: {
					onMainWindowLoad: /* @__PURE__ */ new Set(),
					onMainWindowUnload: /* @__PURE__ */ new Set(),
					onPluginUnload: /* @__PURE__ */ new Set()
				},
				_mainWindow: void 0,
				_plugin: void 0
			}
		};
		try {
			if (typeof globalThis.ChromeUtils?.importESModule !== "undefined" || typeof globalThis.ChromeUtils?.import !== "undefined") {
				const { ConsoleAPI } = _importESModule("resource://gre/modules/Console.sys.mjs");
				this._console = new ConsoleAPI({ consoleID: `${this._basicOptions.api.pluginID}-${Date.now()}` });
			}
		} catch {}
		this.updateOptions(data);
	}
	getGlobal(k) {
		if (typeof globalThis[k] !== "undefined") return globalThis[k];
		const _Zotero = BasicTool.getZotero();
		try {
			const window$1 = _Zotero.getMainWindow();
			switch (k) {
				case "Zotero":
				case "zotero": return _Zotero;
				case "window": return window$1;
				case "windows": return _Zotero.getMainWindows();
				case "document": return window$1.document;
				case "ZoteroPane":
				case "ZoteroPane_Local": return _Zotero.getActiveZoteroPane();
				default: return window$1[k];
			}
		} catch (e) {
			Zotero.logError(e);
		}
	}
	/**
	* If it's an XUL element
	* @param elem
	*/
	isXULElement(elem) {
		return elem.namespaceURI === "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
	}
	/**
	* Create an XUL element
	*
	* For Zotero 6, use `createElementNS`;
	*
	* For Zotero 7+, use `createXULElement`.
	* @param doc
	* @param type
	* @example
	* Create a `<menuitem>`:
	* ```ts
	* const compat = new ZoteroCompat();
	* const doc = compat.getWindow().document;
	* const elem = compat.createXULElement(doc, "menuitem");
	* ```
	*/
	createXULElement(doc, type) {
		return doc.createXULElement(type);
	}
	/**
	* Output to both Zotero.debug and console.log
	* @param data e.g. string, number, object, ...
	*/
	log(...data) {
		if (data.length === 0) return;
		let _Zotero;
		try {
			if (typeof Zotero !== "undefined") _Zotero = Zotero;
			else _Zotero = BasicTool.getZotero();
		} catch {}
		let options;
		if (data[data.length - 1]?._type === "toolkitlog") options = data.pop();
		else options = this._basicOptions.log;
		try {
			if (options.prefix) data.splice(0, 0, options.prefix);
			if (!options.disableConsole) {
				let _console;
				if (typeof console !== "undefined") _console = console;
				else if (_Zotero) _console = _Zotero.getMainWindow()?.console;
				if (!_console) {
					if (!this._console) return;
					_console = this._console;
				}
				if (_console.groupCollapsed) _console.groupCollapsed(...data);
				else _console.group(...data);
				_console.trace();
				_console.groupEnd();
			}
			if (!options.disableZLog) {
				if (typeof _Zotero === "undefined") return;
				_Zotero.debug(data.map((d) => {
					try {
						return typeof d === "object" ? JSON.stringify(d) : String(d);
					} catch {
						_Zotero.debug(d);
						return "";
					}
				}).join("\n"));
			}
		} catch (e) {
			if (_Zotero) Zotero.logError(e);
			else console.error(e);
		}
	}
	/**
	* Patch a function
	* @deprecated Use {@link PatchHelper} instead.
	* @param object The owner of the function
	* @param funcSign The signature of the function(function name)
	* @param ownerSign The signature of patch owner to avoid patching again
	* @param patcher The new wrapper of the patched function
	*/
	patch(object, funcSign, ownerSign, patcher) {
		if (object[funcSign][ownerSign]) throw new Error(`${String(funcSign)} re-patched`);
		this.log("patching", funcSign, `by ${ownerSign}`);
		object[funcSign] = patcher(object[funcSign]);
		object[funcSign][ownerSign] = true;
	}
	/**
	* Add a Zotero event listener callback
	* @param type Event type
	* @param callback Event callback
	*/
	addListenerCallback(type, callback) {
		if (["onMainWindowLoad", "onMainWindowUnload"].includes(type)) this._ensureMainWindowListener();
		if (type === "onPluginUnload") this._ensurePluginListener();
		this._basicOptions.listeners.callbacks[type].add(callback);
	}
	/**
	* Remove a Zotero event listener callback
	* @param type Event type
	* @param callback Event callback
	*/
	removeListenerCallback(type, callback) {
		this._basicOptions.listeners.callbacks[type].delete(callback);
		this._ensureRemoveListener();
	}
	/**
	* Remove all Zotero event listener callbacks when the last callback is removed.
	*/
	_ensureRemoveListener() {
		const { listeners } = this._basicOptions;
		if (listeners._mainWindow && listeners.callbacks.onMainWindowLoad.size === 0 && listeners.callbacks.onMainWindowUnload.size === 0) {
			Services.wm.removeListener(listeners._mainWindow);
			delete listeners._mainWindow;
		}
		if (listeners._plugin && listeners.callbacks.onPluginUnload.size === 0) {
			Zotero.Plugins.removeObserver(listeners._plugin);
			delete listeners._plugin;
		}
	}
	/**
	* Ensure the main window listener is registered.
	*/
	_ensureMainWindowListener() {
		if (this._basicOptions.listeners._mainWindow) return;
		const mainWindowListener = {
			onOpenWindow: (xulWindow) => {
				const domWindow = xulWindow.docShell.domWindow;
				const onload = async () => {
					domWindow.removeEventListener("load", onload, false);
					if (domWindow.location.href !== "chrome://zotero/content/zoteroPane.xhtml") return;
					for (const cbk of this._basicOptions.listeners.callbacks.onMainWindowLoad) try {
						cbk(domWindow);
					} catch (e) {
						this.log(e);
					}
				};
				domWindow.addEventListener("load", () => onload(), false);
			},
			onCloseWindow: async (xulWindow) => {
				const domWindow = xulWindow.docShell.domWindow;
				if (domWindow.location.href !== "chrome://zotero/content/zoteroPane.xhtml") return;
				for (const cbk of this._basicOptions.listeners.callbacks.onMainWindowUnload) try {
					cbk(domWindow);
				} catch (e) {
					this.log(e);
				}
			}
		};
		this._basicOptions.listeners._mainWindow = mainWindowListener;
		Services.wm.addListener(mainWindowListener);
	}
	/**
	* Ensure the plugin listener is registered.
	*/
	_ensurePluginListener() {
		if (this._basicOptions.listeners._plugin) return;
		const pluginListener = { shutdown: (...args) => {
			for (const cbk of this._basicOptions.listeners.callbacks.onPluginUnload) try {
				cbk(...args);
			} catch (e) {
				this.log(e);
			}
		} };
		this._basicOptions.listeners._plugin = pluginListener;
		Zotero.Plugins.addObserver(pluginListener);
	}
	updateOptions(source) {
		if (!source) return this;
		if (source instanceof BasicTool) this._basicOptions = source._basicOptions;
		else this._basicOptions = source;
		return this;
	}
	static getZotero() {
		if (typeof Zotero !== "undefined") return Zotero;
		const { Zotero: _Zotero } = ChromeUtils.importESModule("chrome://zotero/content/zotero.mjs");
		return _Zotero;
	}
};
var ManagerTool = class extends BasicTool {
	_ensureAutoUnregisterAll() {
		this.addListenerCallback("onPluginUnload", (params, _reason) => {
			if (params.id !== this.basicOptions.api.pluginID) return;
			this.unregisterAll();
		});
	}
};
function unregister(tools) {
	Object.values(tools).forEach((tool) => {
		if (tool instanceof ManagerTool || typeof tool?.unregisterAll === "function") tool.unregisterAll();
	});
}
function makeHelperTool(cls, options) {
	return new Proxy(cls, { construct(target, args) {
		const _origin = new cls(...args);
		if (_origin instanceof BasicTool) _origin.updateOptions(options);
		else _origin._version = BasicTool._version;
		return _origin;
	} });
}
function _importESModule(path) {
	if (typeof ChromeUtils.import === "undefined") return ChromeUtils.importESModule(path, { global: "contextual" });
	if (path.endsWith(".sys.mjs")) path = path.replace(/\.sys\.mjs$/, ".jsm");
	return ChromeUtils.import(path);
}

//#endregion
//#region src/helpers/clipboard.ts
/**
* Copy helper for text/richtext/image.
*
* @example
* Copy plain text
* ```ts
* new ClipboardHelper().addText("plain", "text/unicode").copy();
* ```
* @example
* Copy plain text & rich text
* ```ts
* new ClipboardHelper().addText("plain", "text/unicode")
*                     .addText("<h1>rich text</h1>", "text/html")
*                     .copy();
* ```
* @example
* Copy plain text, rich text & image
* ```ts
* new ClipboardHelper().addText("plain", "text/unicode")
*                     .addText("<h1>rich text</h1>", "text/html")
*                     .addImage("data:image/png;base64,...")
*                     .copy();
* ```
*/
var ClipboardHelper = class extends BasicTool {
	transferable;
	clipboardService;
	filePath = "";
	constructor() {
		super();
		this.transferable = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable);
		this.clipboardService = Components.classes["@mozilla.org/widget/clipboard;1"].getService(Components.interfaces.nsIClipboard);
		this.transferable.init(null);
	}
	addText(source, type = "text/plain") {
		const str = Components.classes["@mozilla.org/supports-string;1"].createInstance(Components.interfaces.nsISupportsString);
		str.data = source;
		if (type === "text/unicode") type = "text/plain";
		this.transferable.addDataFlavor(type);
		this.transferable.setTransferData(type, str, source.length * 2);
		return this;
	}
	addImage(source) {
		const parts = source.split(",");
		if (!parts[0].includes("base64")) return this;
		const mime = parts[0].match(/:(.*?);/)[1];
		const bstr = this.getGlobal("window").atob(parts[1]);
		let n = bstr.length;
		const u8arr = new Uint8Array(n);
		while (n--) u8arr[n] = bstr.charCodeAt(n);
		const imgTools = Components.classes["@mozilla.org/image/tools;1"].getService(Components.interfaces.imgITools);
		let mimeType;
		let img;
		if (this.getGlobal("Zotero").platformMajorVersion >= 102) {
			img = imgTools.decodeImageFromArrayBuffer(u8arr.buffer, mime);
			mimeType = "application/x-moz-nativeimage";
		} else {
			mimeType = `image/png`;
			img = Components.classes["@mozilla.org/supports-interface-pointer;1"].createInstance(Components.interfaces.nsISupportsInterfacePointer);
			img.data = imgTools.decodeImageFromArrayBuffer(u8arr.buffer, mimeType);
		}
		this.transferable.addDataFlavor(mimeType);
		this.transferable.setTransferData(mimeType, img, 0);
		return this;
	}
	addFile(path) {
		const file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsIFile);
		file.initWithPath(path);
		this.transferable.addDataFlavor("application/x-moz-file");
		this.transferable.setTransferData("application/x-moz-file", file);
		this.filePath = path;
		return this;
	}
	copy() {
		try {
			this.clipboardService.setData(this.transferable, null, Components.interfaces.nsIClipboard.kGlobalClipboard);
		} catch (e) {
			if (this.filePath && Zotero.isMac) Zotero.Utilities.Internal.exec(`/usr/bin/osascript`, [`-e`, `set the clipboard to POSIX file "${this.filePath}"`]);
			else throw e;
		}
		return this;
	}
};

//#endregion
//#region src/tools/ui.ts
/**
* UI APIs. Create elements and manage them.
*/
var UITool = class extends BasicTool {
	get basicOptions() {
		return this._basicOptions;
	}
	/**
	* Store elements created with this instance
	*
	* @remarks
	* > What is this for?
	*
	* In bootstrap plugins, elements must be manually maintained and removed on exiting.
	*
	* This API does this for you.
	*/
	elementCache;
	constructor(base) {
		super(base);
		this.elementCache = [];
		if (!this._basicOptions.ui) this._basicOptions.ui = {
			enableElementRecord: true,
			enableElementJSONLog: false,
			enableElementDOMLog: true
		};
	}
	/**
	* Remove all elements created by `createElement`.
	*
	* @remarks
	* > What is this for?
	*
	* In bootstrap plugins, elements must be manually maintained and removed on exiting.
	*
	* This API does this for you.
	*/
	unregisterAll() {
		this.elementCache.forEach((e) => {
			try {
				e?.deref()?.remove();
			} catch (e$1) {
				this.log(e$1);
			}
		});
	}
	createElement(...args) {
		const doc = args[0];
		const tagName = args[1].toLowerCase();
		let props = args[2] || {};
		if (!tagName) return;
		if (typeof args[2] === "string") props = {
			namespace: args[2],
			enableElementRecord: args[3]
		};
		if (typeof props.enableElementJSONLog !== "undefined" && props.enableElementJSONLog || this.basicOptions.ui.enableElementJSONLog) this.log(props);
		props.properties = props.properties || props.directAttributes;
		props.children = props.children || props.subElementOptions;
		let elem;
		if (tagName === "fragment") {
			const fragElem = doc.createDocumentFragment();
			elem = fragElem;
		} else {
			let realElem = props.id && (props.checkExistenceParent ? props.checkExistenceParent : doc).querySelector(`#${props.id}`);
			if (realElem && props.ignoreIfExists) return realElem;
			if (realElem && props.removeIfExists) {
				realElem.remove();
				realElem = void 0;
			}
			if (props.customCheck && !props.customCheck(doc, props)) return void 0;
			if (!realElem || !props.skipIfExists) {
				let namespace = props.namespace;
				if (!namespace) {
					const mightHTML = HTMLElementTagNames.includes(tagName);
					const mightXUL = XULElementTagNames.includes(tagName);
					const mightSVG = SVGElementTagNames.includes(tagName);
					if (Number(mightHTML) + Number(mightXUL) + Number(mightSVG) > 1) this.log(`[Warning] Creating element ${tagName} with no namespace specified. Found multiply namespace matches.`);
					if (mightHTML) namespace = "html";
					else if (mightXUL) namespace = "xul";
					else if (mightSVG) namespace = "svg";
					else namespace = "html";
				}
				if (namespace === "xul") realElem = this.createXULElement(doc, tagName);
				else realElem = doc.createElementNS({
					html: "http://www.w3.org/1999/xhtml",
					svg: "http://www.w3.org/2000/svg"
				}[namespace], tagName);
				if (typeof props.enableElementRecord !== "undefined" ? props.enableElementRecord : this.basicOptions.ui.enableElementRecord) this.elementCache.push(new WeakRef(realElem));
			}
			if (props.id) realElem.id = props.id;
			if (props.styles && Object.keys(props.styles).length) Object.keys(props.styles).forEach((k) => {
				const v = props.styles[k];
				typeof v !== "undefined" && (realElem.style[k] = v);
			});
			if (props.properties && Object.keys(props.properties).length) Object.keys(props.properties).forEach((k) => {
				const v = props.properties[k];
				typeof v !== "undefined" && (realElem[k] = v);
			});
			if (props.attributes && Object.keys(props.attributes).length) Object.keys(props.attributes).forEach((k) => {
				const v = props.attributes[k];
				typeof v !== "undefined" && realElem.setAttribute(k, String(v));
			});
			if (props.classList?.length) realElem.classList.add(...props.classList);
			if (props.listeners?.length) props.listeners.forEach(({ type, listener, options }) => {
				listener && realElem.addEventListener(type, listener, options);
			});
			elem = realElem;
		}
		if (props.children?.length) {
			const subElements = props.children.map((childProps) => {
				childProps.namespace = childProps.namespace || props.namespace;
				return this.createElement(doc, childProps.tag, childProps);
			}).filter((e) => e);
			elem.append(...subElements);
		}
		if (typeof props.enableElementDOMLog !== "undefined" ? props.enableElementDOMLog : this.basicOptions.ui.enableElementDOMLog) this.log(elem);
		return elem;
	}
	/**
	* Append element(s) to a node.
	* @param properties See {@link ElementProps}
	* @param container The parent node to append to.
	* @returns A Node that is the appended child (aChild),
	*          except when aChild is a DocumentFragment,
	*          in which case the empty DocumentFragment is returned.
	*/
	appendElement(properties, container) {
		return container.appendChild(this.createElement(container.ownerDocument, properties.tag, properties));
	}
	/**
	* Inserts a node before a reference node as a child of its parent node.
	* @param properties See {@link ElementProps}
	* @param referenceNode The node before which newNode is inserted.
	* @returns Node
	*/
	insertElementBefore(properties, referenceNode) {
		if (referenceNode.parentNode) return referenceNode.parentNode.insertBefore(this.createElement(referenceNode.ownerDocument, properties.tag, properties), referenceNode);
		else this.log(`${referenceNode.tagName} has no parent, cannot insert ${properties.tag}`);
	}
	/**
	* Replace oldNode with a new one.
	* @param properties See {@link ElementProps}
	* @param oldNode The child to be replaced.
	* @returns The replaced Node. This is the same node as oldChild.
	*/
	replaceElement(properties, oldNode) {
		if (oldNode.parentNode) return oldNode.parentNode.replaceChild(this.createElement(oldNode.ownerDocument, properties.tag, properties), oldNode);
		else this.log(`${oldNode.tagName} has no parent, cannot replace it with ${properties.tag}`);
	}
	/**
	* Parse XHTML to XUL fragment. For Zotero 6.
	*
	* To load preferences from a Zotero 7's `.xhtml`, use this method to parse it.
	* @param str xhtml raw text
	* @param entities dtd file list ("chrome://xxx.dtd")
	* @param defaultXUL true for default XUL namespace
	*/
	parseXHTMLToFragment(str, entities = [], defaultXUL = true) {
		const parser = new DOMParser();
		const xulns = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
		const htmlns = "http://www.w3.org/1999/xhtml";
		const wrappedStr = `${entities.length ? `<!DOCTYPE bindings [ ${entities.reduce((preamble, url, index) => {
			return `${preamble}<!ENTITY % _dtd-${index} SYSTEM "${url}"> %_dtd-${index}; `;
		}, "")}]>` : ""}
      <html:div xmlns="${defaultXUL ? xulns : htmlns}"
          xmlns:xul="${xulns}" xmlns:html="${htmlns}">
      ${str}
      </html:div>`;
		this.log(wrappedStr, parser);
		const doc = parser.parseFromString(wrappedStr, "text/xml");
		this.log(doc);
		if (doc.documentElement.localName === "parsererror") throw new Error("not well-formed XHTML");
		const range = doc.createRange();
		range.selectNodeContents(doc.querySelector("div"));
		return range.extractContents();
	}
};
const HTMLElementTagNames = [
	"a",
	"abbr",
	"address",
	"area",
	"article",
	"aside",
	"audio",
	"b",
	"base",
	"bdi",
	"bdo",
	"blockquote",
	"body",
	"br",
	"button",
	"canvas",
	"caption",
	"cite",
	"code",
	"col",
	"colgroup",
	"data",
	"datalist",
	"dd",
	"del",
	"details",
	"dfn",
	"dialog",
	"div",
	"dl",
	"dt",
	"em",
	"embed",
	"fieldset",
	"figcaption",
	"figure",
	"footer",
	"form",
	"h1",
	"h2",
	"h3",
	"h4",
	"h5",
	"h6",
	"head",
	"header",
	"hgroup",
	"hr",
	"html",
	"i",
	"iframe",
	"img",
	"input",
	"ins",
	"kbd",
	"label",
	"legend",
	"li",
	"link",
	"main",
	"map",
	"mark",
	"menu",
	"meta",
	"meter",
	"nav",
	"noscript",
	"object",
	"ol",
	"optgroup",
	"option",
	"output",
	"p",
	"picture",
	"pre",
	"progress",
	"q",
	"rp",
	"rt",
	"ruby",
	"s",
	"samp",
	"script",
	"section",
	"select",
	"slot",
	"small",
	"source",
	"span",
	"strong",
	"style",
	"sub",
	"summary",
	"sup",
	"table",
	"tbody",
	"td",
	"template",
	"textarea",
	"tfoot",
	"th",
	"thead",
	"time",
	"title",
	"tr",
	"track",
	"u",
	"ul",
	"var",
	"video",
	"wbr"
];
const XULElementTagNames = [
	"action",
	"arrowscrollbox",
	"bbox",
	"binding",
	"bindings",
	"box",
	"broadcaster",
	"broadcasterset",
	"button",
	"browser",
	"checkbox",
	"caption",
	"colorpicker",
	"column",
	"columns",
	"commandset",
	"command",
	"conditions",
	"content",
	"deck",
	"description",
	"dialog",
	"dialogheader",
	"editor",
	"grid",
	"grippy",
	"groupbox",
	"hbox",
	"iframe",
	"image",
	"key",
	"keyset",
	"label",
	"listbox",
	"listcell",
	"listcol",
	"listcols",
	"listhead",
	"listheader",
	"listitem",
	"member",
	"menu",
	"menubar",
	"menuitem",
	"menulist",
	"menupopup",
	"menuseparator",
	"observes",
	"overlay",
	"page",
	"popup",
	"popupset",
	"preference",
	"preferences",
	"prefpane",
	"prefwindow",
	"progressmeter",
	"radio",
	"radiogroup",
	"resizer",
	"richlistbox",
	"richlistitem",
	"row",
	"rows",
	"rule",
	"script",
	"scrollbar",
	"scrollbox",
	"scrollcorner",
	"separator",
	"spacer",
	"splitter",
	"stack",
	"statusbar",
	"statusbarpanel",
	"stringbundle",
	"stringbundleset",
	"tab",
	"tabbrowser",
	"tabbox",
	"tabpanel",
	"tabpanels",
	"tabs",
	"template",
	"textnode",
	"textbox",
	"titlebar",
	"toolbar",
	"toolbarbutton",
	"toolbargrippy",
	"toolbaritem",
	"toolbarpalette",
	"toolbarseparator",
	"toolbarset",
	"toolbarspacer",
	"toolbarspring",
	"toolbox",
	"tooltip",
	"tree",
	"treecell",
	"treechildren",
	"treecol",
	"treecols",
	"treeitem",
	"treerow",
	"treeseparator",
	"triple",
	"vbox",
	"window",
	"wizard",
	"wizardpage"
];
const SVGElementTagNames = [
	"a",
	"animate",
	"animateMotion",
	"animateTransform",
	"circle",
	"clipPath",
	"defs",
	"desc",
	"ellipse",
	"feBlend",
	"feColorMatrix",
	"feComponentTransfer",
	"feComposite",
	"feConvolveMatrix",
	"feDiffuseLighting",
	"feDisplacementMap",
	"feDistantLight",
	"feDropShadow",
	"feFlood",
	"feFuncA",
	"feFuncB",
	"feFuncG",
	"feFuncR",
	"feGaussianBlur",
	"feImage",
	"feMerge",
	"feMergeNode",
	"feMorphology",
	"feOffset",
	"fePointLight",
	"feSpecularLighting",
	"feSpotLight",
	"feTile",
	"feTurbulence",
	"filter",
	"foreignObject",
	"g",
	"image",
	"line",
	"linearGradient",
	"marker",
	"mask",
	"metadata",
	"mpath",
	"path",
	"pattern",
	"polygon",
	"polyline",
	"radialGradient",
	"rect",
	"script",
	"set",
	"stop",
	"style",
	"svg",
	"switch",
	"symbol",
	"text",
	"textPath",
	"title",
	"tspan",
	"use",
	"view"
];

//#endregion
//#region src/helpers/dialog.ts
/**
* Dialog window helper. A superset of XUL dialog.
*/
var DialogHelper = class extends UITool {
	/**
	* Passed to dialog window for data-binding and lifecycle controls. See {@link DialogHelper.setDialogData}
	*/
	dialogData;
	/**
	* Dialog window instance
	*/
	window;
	elementProps;
	/**
	* Create a dialog helper with row \* column grids.
	* @param row
	* @param column
	*/
	constructor(row, column) {
		super();
		if (row <= 0 || column <= 0) throw new Error(`row and column must be positive integers.`);
		this.elementProps = {
			tag: "vbox",
			attributes: { flex: 1 },
			styles: {
				width: "100%",
				height: "100%"
			},
			children: []
		};
		for (let i = 0; i < Math.max(row, 1); i++) {
			this.elementProps.children.push({
				tag: "hbox",
				attributes: { flex: 1 },
				children: []
			});
			for (let j = 0; j < Math.max(column, 1); j++) this.elementProps.children[i].children.push({
				tag: "vbox",
				attributes: { flex: 1 },
				children: []
			});
		}
		this.elementProps.children.push({
			tag: "hbox",
			attributes: {
				flex: 0,
				pack: "end"
			},
			children: []
		});
		this.dialogData = {};
	}
	/**
	* Add a cell at (row, column). Index starts from 0.
	* @param row
	* @param column
	* @param elementProps Cell element props. See {@link ElementProps}
	* @param cellFlex If the cell is flex. Default true.
	*/
	addCell(row, column, elementProps, cellFlex = true) {
		if (row >= this.elementProps.children.length || column >= this.elementProps.children[row].children.length) throw new Error(`Cell index (${row}, ${column}) is invalid, maximum (${this.elementProps.children.length}, ${this.elementProps.children[0].children.length})`);
		this.elementProps.children[row].children[column].children = [elementProps];
		this.elementProps.children[row].children[column].attributes.flex = cellFlex ? 1 : 0;
		return this;
	}
	/**
	* Add a control button to the bottom of the dialog.
	* @param label Button label
	* @param id Button id.
	* The corresponding id of the last button user clicks before window exit will be set to `dialogData._lastButtonId`.
	* @param options Options
	* @param [options.noClose] Don't close window when clicking this button.
	* @param [options.callback] Callback of button click event.
	*/
	addButton(label, id, options = {}) {
		id = id || `btn-${Zotero.Utilities.randomString()}-${(/* @__PURE__ */ new Date()).getTime()}`;
		this.elementProps.children[this.elementProps.children.length - 1].children.push({
			tag: "vbox",
			styles: { margin: "10px" },
			children: [{
				tag: "button",
				namespace: "html",
				id,
				attributes: {
					type: "button",
					"data-l10n-id": label
				},
				properties: { innerHTML: label },
				listeners: [{
					type: "click",
					listener: (e) => {
						this.dialogData._lastButtonId = id;
						if (options.callback) options.callback(e);
						if (!options.noClose) this.window.close();
					}
				}]
			}]
		});
		return this;
	}
	/**
	* Dialog data.
	* @remarks
	* This object is passed to the dialog window.
	*
	* The control button id is in `dialogData._lastButtonId`;
	*
	* The data-binding values are in `dialogData`.
	* ```ts
	* interface DialogData {
	*   [key: string | number | symbol]: any;
	*   loadLock?: { promise: Promise<void>; resolve: () => void; isResolved: () => boolean }; // resolve after window load (auto-generated)
	*   loadCallback?: Function; // called after window load
	*   unloadLock?: { promise: Promise<void>; resolve: () => void }; // resolve after window unload (auto-generated)
	*   unloadCallback?: Function; // called after window unload
	*   beforeUnloadCallback?: Function; // called before window unload when elements are accessable.
	* }
	* ```
	* @param dialogData
	*/
	setDialogData(dialogData) {
		this.dialogData = dialogData;
		return this;
	}
	/**
	* Open the dialog
	* @param title Window title
	* @param windowFeatures
	* @param windowFeatures.width Ignored if fitContent is `true`.
	* @param windowFeatures.height Ignored if fitContent is `true`.
	* @param windowFeatures.left
	* @param windowFeatures.top
	* @param windowFeatures.centerscreen Open window at the center of screen.
	* @param windowFeatures.resizable If window is resizable.
	* @param windowFeatures.fitContent Resize the window to content size after elements are loaded.
	* @param windowFeatures.noDialogMode Dialog mode window only has a close button. Set `true` to make maximize and minimize button visible.
	* @param windowFeatures.alwaysRaised Is the window always at the top.
	*/
	open(title, windowFeatures = {
		centerscreen: true,
		resizable: true,
		fitContent: true
	}) {
		this.window = openDialog(this, `dialog-${Zotero.Utilities.randomString()}-${(/* @__PURE__ */ new Date()).getTime()}`, title, this.elementProps, this.dialogData, windowFeatures);
		return this;
	}
};
function openDialog(dialogHelper, targetId, title, elementProps, dialogData, windowFeatures = {
	centerscreen: true,
	resizable: true,
	fitContent: true
}) {
	dialogData = dialogData || {};
	if (!dialogData.loadLock) {
		let loadResolve;
		let isLoadResolved = false;
		const loadPromise = new Promise((resolve) => {
			loadResolve = resolve;
		});
		loadPromise.then(() => {
			isLoadResolved = true;
		});
		dialogData.loadLock = {
			promise: loadPromise,
			resolve: loadResolve,
			isResolved: () => isLoadResolved
		};
	}
	if (!dialogData.unloadLock) {
		let unloadResolve;
		const unloadPromise = new Promise((resolve) => {
			unloadResolve = resolve;
		});
		dialogData.unloadLock = {
			promise: unloadPromise,
			resolve: unloadResolve
		};
	}
	let featureString = `resizable=${windowFeatures.resizable ? "yes" : "no"},`;
	if (windowFeatures.width || windowFeatures.height) featureString += `width=${windowFeatures.width || 100},height=${windowFeatures.height || 100},`;
	if (windowFeatures.left) featureString += `left=${windowFeatures.left},`;
	if (windowFeatures.top) featureString += `top=${windowFeatures.top},`;
	if (windowFeatures.centerscreen) featureString += "centerscreen,";
	if (windowFeatures.noDialogMode) featureString += "dialog=no,";
	if (windowFeatures.alwaysRaised) featureString += "alwaysRaised=yes,";
	const win = dialogHelper.getGlobal("openDialog")("about:blank", targetId || "_blank", featureString, dialogData);
	dialogData.loadLock?.promise.then(() => {
		win.document.head.appendChild(dialogHelper.createElement(win.document, "title", {
			properties: { innerText: title },
			attributes: { "data-l10n-id": title }
		}));
		let l10nFiles = dialogData.l10nFiles || [];
		if (typeof l10nFiles === "string") l10nFiles = [l10nFiles];
		l10nFiles.forEach((file) => {
			win.document.head.appendChild(dialogHelper.createElement(win.document, "link", { properties: {
				rel: "localization",
				href: file
			} }));
		});
		dialogHelper.appendElement({
			tag: "fragment",
			children: [
				{
					tag: "style",
					properties: { innerHTML: style }
				},
				{
					tag: "link",
					properties: {
						rel: "stylesheet",
						href: "chrome://global/skin/global.css"
					}
				},
				{
					tag: "link",
					properties: {
						rel: "stylesheet",
						href: "chrome://zotero-platform/content/zotero.css"
					}
				}
			]
		}, win.document.head);
		replaceElement(elementProps, dialogHelper);
		win.document.body.appendChild(dialogHelper.createElement(win.document, "fragment", { children: [elementProps] }));
		Array.from(win.document.querySelectorAll("*[data-bind]")).forEach((elem) => {
			const bindKey = elem.getAttribute("data-bind");
			const bindAttr = elem.getAttribute("data-attr");
			const bindProp = elem.getAttribute("data-prop");
			if (bindKey && dialogData && dialogData[bindKey]) if (bindProp) elem[bindProp] = dialogData[bindKey];
			else elem.setAttribute(bindAttr || "value", dialogData[bindKey]);
		});
		if (windowFeatures.fitContent) setTimeout(() => {
			win.sizeToContent();
		}, 300);
		win.focus();
	}).then(() => {
		dialogData?.loadCallback && dialogData.loadCallback();
	});
	dialogData.unloadLock?.promise.then(() => {
		dialogData?.unloadCallback && dialogData.unloadCallback();
	});
	win.addEventListener("DOMContentLoaded", function onWindowLoad(_ev) {
		win.arguments[0]?.loadLock?.resolve();
		win.removeEventListener("DOMContentLoaded", onWindowLoad, false);
	}, false);
	win.addEventListener("beforeunload", function onWindowBeforeUnload(_ev) {
		Array.from(win.document.querySelectorAll("*[data-bind]")).forEach((elem) => {
			const dialogData$1 = this.window.arguments[0];
			const bindKey = elem.getAttribute("data-bind");
			const bindAttr = elem.getAttribute("data-attr");
			const bindProp = elem.getAttribute("data-prop");
			if (bindKey && dialogData$1) if (bindProp) dialogData$1[bindKey] = elem[bindProp];
			else dialogData$1[bindKey] = elem.getAttribute(bindAttr || "value");
		});
		this.window.removeEventListener("beforeunload", onWindowBeforeUnload, false);
		dialogData?.beforeUnloadCallback && dialogData.beforeUnloadCallback();
	});
	win.addEventListener("unload", function onWindowUnload(_ev) {
		if (!this.window.arguments[0]?.loadLock?.isResolved()) return;
		this.window.arguments[0]?.unloadLock?.resolve();
		this.window.removeEventListener("unload", onWindowUnload, false);
	});
	if (win.document.readyState === "complete") win.arguments[0]?.loadLock?.resolve();
	return win;
}
function replaceElement(elementProps, uiTool) {
	let checkChildren = true;
	if (elementProps.tag === "select") {
		let is140 = false;
		try {
			is140 = Number.parseInt(Services.appinfo.platformVersion.match(/^\d+/)[0]) >= 140;
		} catch {
			is140 = false;
		}
		if (!is140) {
			checkChildren = false;
			const customSelectProps = {
				tag: "div",
				classList: ["dropdown"],
				listeners: [{
					type: "mouseleave",
					listener: (ev) => {
						const select = ev.target.querySelector("select");
						select?.blur();
					}
				}],
				children: [Object.assign({}, elementProps, {
					tag: "select",
					listeners: [{
						type: "focus",
						listener: (ev) => {
							const select = ev.target;
							const dropdown = select.parentElement?.querySelector(".dropdown-content");
							dropdown && (dropdown.style.display = "block");
							select.setAttribute("focus", "true");
						}
					}, {
						type: "blur",
						listener: (ev) => {
							const select = ev.target;
							const dropdown = select.parentElement?.querySelector(".dropdown-content");
							dropdown && (dropdown.style.display = "none");
							select.removeAttribute("focus");
						}
					}]
				}), {
					tag: "div",
					classList: ["dropdown-content"],
					children: elementProps.children?.map((option) => ({
						tag: "p",
						attributes: { value: option.properties?.value },
						properties: { innerHTML: option.properties?.innerHTML || option.properties?.textContent },
						classList: ["dropdown-item"],
						listeners: [{
							type: "click",
							listener: (ev) => {
								const select = ev.target.parentElement?.previousElementSibling;
								select && (select.value = ev.target.getAttribute("value") || "");
								select?.blur();
							}
						}]
					}))
				}]
			};
			for (const key in elementProps) delete elementProps[key];
			Object.assign(elementProps, customSelectProps);
		} else {
			const children = elementProps.children || [];
			const randomString = CSS.escape(`${Zotero.Utilities.randomString()}-${(/* @__PURE__ */ new Date()).getTime()}`);
			if (!elementProps.id) elementProps.id = `select-${randomString}`;
			const selectId = elementProps.id;
			const popupId = `popup-${randomString}`;
			const popup = uiTool.appendElement({
				tag: "menupopup",
				namespace: "xul",
				id: popupId,
				children: children.map((option) => ({
					tag: "menuitem",
					attributes: {
						value: option.properties?.value,
						label: option.properties?.innerHTML || option.properties?.textContent
					}
				})),
				listeners: [{
					type: "command",
					listener: (ev) => {
						if (ev.target?.tagName !== "menuitem") return;
						const select = uiTool.window.document.getElementById(selectId);
						const menuitem = ev.target;
						if (select) {
							select.value = menuitem.getAttribute("value") || "";
							select.blur();
						}
						popup.hidePopup();
					}
				}]
			}, uiTool.window.document.body);
			if (!elementProps.listeners) elementProps.listeners = [];
			elementProps.listeners.push(...[{
				type: "click",
				listener: (ev) => {
					const select = ev.target;
					const rect = select.getBoundingClientRect();
					let left = rect.left + uiTool.window.scrollX;
					let top = rect.bottom + uiTool.window.scrollY;
					if (uiTool.getGlobal("Zotero").isMac) {
						left += uiTool.window.screenLeft;
						top += uiTool.window.screenTop + rect.height;
					}
					fixMenuPopup(popup, uiTool);
					popup.openPopup(null, "", left, top, false, false);
					select.setAttribute("focus", "true");
				}
			}]);
		}
	} else if (elementProps.tag === "a") {
		const href = elementProps?.properties?.href || "";
		elementProps.properties ??= {};
		elementProps.properties.href = "javascript:void(0);";
		elementProps.attributes ??= {};
		elementProps.attributes["zotero-href"] = href;
		elementProps.listeners ??= [];
		elementProps.listeners.push({
			type: "click",
			listener: (ev) => {
				const href$1 = ev.target?.getAttribute("zotero-href");
				href$1 && uiTool.getGlobal("Zotero").launchURL(href$1);
			}
		});
		elementProps.classList ??= [];
		elementProps.classList.push("zotero-text-link");
	}
	if (checkChildren) elementProps.children?.forEach((child) => replaceElement(child, uiTool));
}
const style = `
html {
  color-scheme: light dark;
}
.zotero-text-link {
  -moz-user-focus: normal;
  color: -moz-nativehyperlinktext;
  text-decoration: underline;
  border: 1px solid transparent;
  cursor: pointer;
}
.dropdown {
  position: relative;
  display: inline-block;
}
.dropdown-content {
  display: none;
  position: absolute;
  background-color: var(--material-toolbar);
  min-width: 160px;
  box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.5);
  border-radius: 5px;
  padding: 5px 0 5px 0;
  z-index: 999;
}
.dropdown-item {
  margin: 0px;
  padding: 5px 10px 5px 10px;
}
.dropdown-item:hover {
  background-color: var(--fill-quinary);
}
`;
function fixMenuPopup(popup, uiTool) {
	for (const item of popup.querySelectorAll("menuitem")) if (!item.innerHTML) uiTool.appendElement({
		tag: "fragment",
		children: [
			{
				tag: "image",
				namespace: "xul",
				classList: ["menu-icon"],
				attributes: { "aria-hidden": "true" }
			},
			{
				tag: "label",
				namespace: "xul",
				classList: ["menu-text"],
				properties: { value: item.getAttribute("label") || "" },
				attributes: {
					crop: "end",
					"aria-hidden": "true"
				}
			},
			{
				tag: "label",
				namespace: "xul",
				classList: ["menu-highlightable-text"],
				properties: { textContent: item.getAttribute("label") || "" },
				attributes: {
					crop: "end",
					"aria-hidden": "true"
				}
			},
			{
				tag: "label",
				namespace: "xul",
				classList: ["menu-accel"],
				attributes: { "aria-hidden": "true" }
			}
		]
	}, item);
}

//#endregion
//#region src/helpers/filePicker.ts
/**
* File picker helper.
* @param title window title
* @param mode
* @param filters Array<[hint string, filter string]>
* @param suggestion default file/folder
* @param window the parent window. By default it is the main window
* @param filterMask built-in filters
* @param directory directory in which to open the file picker
* @example
* ```ts
* await new FilePickerHelper(
*   `${Zotero.getString("fileInterface.import")} MarkDown Document`,
*   "open",
*   [["MarkDown File(*.md)", "*.md"]]
* ).open();
* ```
*/
var FilePickerHelper = class extends BasicTool {
	title;
	mode;
	filters;
	suggestion;
	directory;
	window;
	filterMask;
	constructor(title, mode, filters, suggestion, window$1, filterMask, directory) {
		super();
		this.title = title;
		this.mode = mode;
		this.filters = filters;
		this.suggestion = suggestion;
		this.directory = directory;
		this.window = window$1;
		this.filterMask = filterMask;
	}
	async open() {
		const Backend = ChromeUtils.importESModule("chrome://zotero/content/modules/filePicker.mjs").FilePicker;
		const fp = new Backend();
		fp.init(this.window || this.getGlobal("window"), this.title, this.getMode(fp));
		for (const [label, ext] of this.filters || []) fp.appendFilter(label, ext);
		if (this.filterMask) fp.appendFilters(this.getFilterMask(fp));
		if (this.suggestion) fp.defaultString = this.suggestion;
		if (this.directory) fp.displayDirectory = this.directory;
		const userChoice = await fp.show();
		switch (userChoice) {
			case fp.returnOK:
			case fp.returnReplace: return this.mode === "multiple" ? fp.files : fp.file;
			default: return false;
		}
	}
	getMode(fp) {
		switch (this.mode) {
			case "open": return fp.modeOpen;
			case "save": return fp.modeSave;
			case "folder": return fp.modeGetFolder;
			case "multiple": return fp.modeOpenMultiple;
			default: return 0;
		}
	}
	getFilterMask(fp) {
		switch (this.filterMask) {
			case "all": return fp.filterAll;
			case "html": return fp.filterHTML;
			case "text": return fp.filterText;
			case "images": return fp.filterImages;
			case "xml": return fp.filterXML;
			case "apps": return fp.filterApps;
			case "urls": return fp.filterAllowURLs;
			case "audio": return fp.filterAudio;
			case "video": return fp.filterVideo;
			default: return 1;
		}
	}
};

//#endregion
//#region src/helpers/guide.ts
/**
* Helper for creating a guide.
* Designed for creating a step-by-step guide for users.
* @alpha
*/
var GuideHelper = class extends BasicTool {
	_steps = [];
	constructor() {
		super();
	}
	addStep(step) {
		this._steps.push(step);
		return this;
	}
	addSteps(steps) {
		this._steps.push(...steps);
		return this;
	}
	async show(doc) {
		if (!doc?.ownerGlobal) throw new Error("Document is required.");
		const guide = new Guide(doc.ownerGlobal);
		await guide.show(this._steps);
		const promise = new Promise((resolve) => {
			guide._panel.addEventListener("guide-finished", () => resolve(guide));
		});
		await promise;
		return guide;
	}
	async highlight(doc, step) {
		if (!doc?.ownerGlobal) throw new Error("Document is required.");
		const guide = new Guide(doc.ownerGlobal);
		await guide.show([step]);
		const promise = new Promise((resolve) => {
			guide._panel.addEventListener("guide-finished", () => resolve(guide));
		});
		await promise;
		return guide;
	}
};
var Guide = class {
	_window;
	_id = `guide-${Zotero.Utilities.randomString()}`;
	_panel;
	_header;
	_body;
	_footer;
	_progress;
	_closeButton;
	_prevButton;
	_nextButton;
	_steps;
	_noClose;
	_closed;
	_autoNext;
	_currentIndex;
	initialized;
	_cachedMasks = [];
	get content() {
		return this._window.MozXULElement.parseXULToFragment(`
      <panel id="${this._id}" class="guide-panel" type="arrow" align="top" noautohide="true">
          <html:div class="guide-panel-content">
              <html:div class="guide-panel-header"></html:div>
              <html:div class="guide-panel-body"></html:div>
              <html:div class="guide-panel-footer">
                  <html:div class="guide-panel-progress"></html:div>
                  <html:div class="guide-panel-buttons">
                      <button id="prev-button" class="guide-panel-button" hidden="true"></button>
                      <button id="next-button" class="guide-panel-button" hidden="true"></button>
                      <button id="close-button" class="guide-panel-button" hidden="true"></button>
                  </html:div>
              </html:div>
          </html:div>
          <html:style>
              .guide-panel {
                  background-color: var(--material-menu);
                  color: var(--fill-primary);
              }
              .guide-panel-content {
                  display: flex;
                  flex-direction: column;
                  padding: 0;
              }
              .guide-panel-header {
                  font-size: 1.2em;
                  font-weight: bold;
                  margin-bottom: 10px;
              }
              .guide-panel-header:empty {
                display: none;
              }
              .guide-panel-body {
                  align-items: center;
                  display: flex;
                  flex-direction: column;
                  white-space: pre-wrap;
              }
              .guide-panel-body:empty {
                display: none;
              }
              .guide-panel-footer {
                  display: flex;
                  flex-direction: row;
                  align-items: center;
                  justify-content: space-between;
                  margin-top: 10px;
              }
              .guide-panel-progress {
                  font-size: 0.8em;
              }
              .guide-panel-buttons {
                  display: flex;
                  flex-direction: row;
                  flex-grow: 1;
                  justify-content: flex-end;
              }
          </html:style>
      </panel>
  `);
	}
	get currentStep() {
		if (!this._steps) return void 0;
		return this._steps[this._currentIndex];
	}
	get currentTarget() {
		const step = this.currentStep;
		if (!step?.element) return void 0;
		let elem;
		if (typeof step.element === "function") elem = step.element();
		else if (typeof step.element === "string") elem = this._window.document.querySelector(step.element);
		else if (!step.element) elem = this._window.document.documentElement || void 0;
		else elem = step.element;
		return elem;
	}
	get hasNext() {
		return this._steps && this._currentIndex < this._steps.length - 1;
	}
	get hasPrevious() {
		return this._steps && this._currentIndex > 0;
	}
	get hookProps() {
		return {
			config: this.currentStep,
			state: {
				step: this._currentIndex,
				steps: this._steps,
				controller: this
			}
		};
	}
	get panel() {
		return this._panel;
	}
	constructor(win) {
		this._window = win;
		this._noClose = false;
		this._closed = false;
		this._autoNext = true;
		this._currentIndex = 0;
		const doc = win.document;
		const content = this.content;
		if (content) doc.documentElement?.append(doc.importNode(content, true));
		this._panel = doc.querySelector(`#${this._id}`);
		this._header = this._panel.querySelector(".guide-panel-header");
		this._body = this._panel.querySelector(".guide-panel-body");
		this._footer = this._panel.querySelector(".guide-panel-footer");
		this._progress = this._panel.querySelector(".guide-panel-progress");
		this._closeButton = this._panel.querySelector("#close-button");
		this._prevButton = this._panel.querySelector("#prev-button");
		this._nextButton = this._panel.querySelector("#next-button");
		this._closeButton.addEventListener("click", async () => {
			if (this.currentStep?.onCloseClick) await this.currentStep.onCloseClick(this.hookProps);
			this.abort();
		});
		this._prevButton.addEventListener("click", async () => {
			if (this.currentStep?.onPrevClick) await this.currentStep.onPrevClick(this.hookProps);
			this.movePrevious();
		});
		this._nextButton.addEventListener("click", async () => {
			if (this.currentStep?.onNextClick) await this.currentStep.onNextClick(this.hookProps);
			this.moveNext();
		});
		this._panel.addEventListener("popupshown", this._handleShown.bind(this));
		this._panel.addEventListener("popuphidden", this._handleHidden.bind(this));
		this._window.addEventListener("resize", this._centerPanel);
	}
	async show(steps) {
		if (steps) {
			this._steps = steps;
			this._currentIndex = 0;
		}
		const index = this._currentIndex;
		this._noClose = false;
		this._closed = false;
		this._autoNext = true;
		const step = this.currentStep;
		if (!step) return;
		const elem = this.currentTarget;
		if (step.onBeforeRender) {
			await step.onBeforeRender(this.hookProps);
			if (index !== this._currentIndex) {
				await this.show();
				return;
			}
		}
		if (step.onMask) step.onMask({ mask: (_e) => this._createMask(_e) });
		else this._createMask(elem);
		let x;
		let y = 0;
		let position = step.position || "after_start";
		if (position === "center") {
			position = "overlap";
			x = this._window.innerWidth / 2;
			y = this._window.innerHeight / 2;
		}
		this._panel.openPopup(elem, step.position || "after_start", x, y, false, false);
	}
	hide() {
		this._panel.hidePopup();
	}
	abort() {
		this._closed = true;
		this.hide();
		this._steps = void 0;
	}
	moveTo(stepIndex) {
		if (!this._steps) {
			this.hide();
			return;
		}
		if (stepIndex < 0) stepIndex = 0;
		if (!this._steps[stepIndex]) {
			this._currentIndex = this._steps.length;
			this.hide();
			return;
		}
		this._autoNext = false;
		this._noClose = true;
		this.hide();
		this._noClose = false;
		this._autoNext = true;
		this._currentIndex = stepIndex;
		this.show();
	}
	moveNext() {
		this.moveTo(this._currentIndex + 1);
	}
	movePrevious() {
		this.moveTo(this._currentIndex - 1);
	}
	_handleShown() {
		if (!this._steps) return;
		const step = this.currentStep;
		if (!step) return;
		this._header.innerHTML = step.title || "";
		this._body.innerHTML = step.description || "";
		this._panel.querySelectorAll(".guide-panel-button").forEach((elem) => {
			elem.hidden = true;
			elem.disabled = false;
		});
		let showButtons = step.showButtons;
		if (!showButtons) {
			showButtons = [];
			if (this.hasPrevious) showButtons.push("prev");
			if (this.hasNext) showButtons.push("next");
			else showButtons.push("close");
		}
		if (showButtons?.length) showButtons.forEach((btn) => {
			this._panel.querySelector(`#${btn}-button`).hidden = false;
		});
		if (step.disableButtons) step.disableButtons.forEach((btn) => {
			this._panel.querySelector(`#${btn}-button`).disabled = true;
		});
		if (step.showProgress) {
			this._progress.hidden = false;
			this._progress.textContent = step.progressText || `${this._currentIndex + 1}/${this._steps.length}`;
		} else this._progress.hidden = true;
		this._closeButton.label = step.closeBtnText || "Done";
		this._nextButton.label = step.nextBtnText || "Next";
		this._prevButton.label = step.prevBtnText || "Previous";
		if (step.onRender) step.onRender(this.hookProps);
		if (step.position === "center") {
			this._centerPanel();
			this._window.setTimeout(this._centerPanel, 10);
		}
	}
	async _handleHidden() {
		this._removeMask();
		this._header.innerHTML = "";
		this._body.innerHTML = "";
		this._progress.textContent = "";
		if (!this._steps) return;
		const step = this.currentStep;
		if (step && step.onExit) await step.onExit(this.hookProps);
		if (!this._noClose && (this._closed || !this.hasNext)) {
			this._panel.dispatchEvent(new this._window.CustomEvent("guide-finished"));
			this._panel.remove();
			this._window.removeEventListener("resize", this._centerPanel);
			return;
		}
		if (this._autoNext) this.moveNext();
	}
	_centerPanel = () => {
		const win = this._window;
		this._panel.moveTo(win.screenX + win.innerWidth / 2 - this._panel.clientWidth / 2, win.screenY + win.innerHeight / 2 - this._panel.clientHeight / 2);
	};
	_createMask(targetElement) {
		const doc = targetElement?.ownerDocument || this._window.document;
		const NS = "http://www.w3.org/2000/svg";
		const svg = doc.createElementNS(NS, "svg");
		svg.id = "guide-panel-mask";
		svg.style.position = "fixed";
		svg.style.top = "0";
		svg.style.left = "0";
		svg.style.width = "100%";
		svg.style.height = "100%";
		svg.style.zIndex = "9999";
		const mask = doc.createElementNS(NS, "mask");
		mask.id = "mask";
		const fullRect = doc.createElementNS(NS, "rect");
		fullRect.setAttribute("x", "0");
		fullRect.setAttribute("y", "0");
		fullRect.setAttribute("width", "100%");
		fullRect.setAttribute("height", "100%");
		fullRect.setAttribute("fill", "white");
		mask.appendChild(fullRect);
		if (targetElement) {
			const rect = targetElement.getBoundingClientRect();
			const targetRect = doc.createElementNS(NS, "rect");
			targetRect.setAttribute("x", rect.left.toString());
			targetRect.setAttribute("y", rect.top.toString());
			targetRect.setAttribute("width", rect.width.toString());
			targetRect.setAttribute("height", rect.height.toString());
			targetRect.setAttribute("fill", "black");
			mask.appendChild(targetRect);
		}
		const maskedRect = doc.createElementNS(NS, "rect");
		maskedRect.setAttribute("x", "0");
		maskedRect.setAttribute("y", "0");
		maskedRect.setAttribute("width", "100%");
		maskedRect.setAttribute("height", "100%");
		maskedRect.setAttribute("mask", "url(#mask)");
		maskedRect.setAttribute("opacity", "0.7");
		svg.appendChild(mask);
		svg.appendChild(maskedRect);
		this._cachedMasks.push(new WeakRef(svg));
		doc.documentElement?.appendChild(svg);
	}
	_removeMask() {
		this._cachedMasks.forEach((ref) => {
			const mask = ref.deref();
			if (mask) mask.remove();
		});
		this._cachedMasks = [];
	}
};

//#endregion
//#region src/helpers/largePref.ts
/**
* Helper class for storing large amounts of data in Zotero preferences.
*
* @remarks
* The allowed data length for a single preference is at least 100k,
* but if this can grow infinitely, like an Array or an Object,
* there will be significant performance problems.
*
* This class stores the keys of data in a single preference as a JSON string of Array,
* and stores the values of data in separate preferences.
*
* You can either use the class as a normal object with `asObject()`,
* or use the methods to access the data.
*/
var LargePrefHelper = class extends BasicTool {
	keyPref;
	valuePrefPrefix;
	innerObj;
	hooks;
	/**
	*
	* @param keyPref The preference name for storing the keys of the data.
	* @param valuePrefPrefix The preference name prefix for storing the values of the data.
	* @param hooks Hooks for parsing the values of the data.
	* - `afterGetValue`: A function that takes the value of the data as input and returns the parsed value.
	* - `beforeSetValue`: A function that takes the key and value of the data as input and returns the parsed key and value.
	* If `hooks` is `"default"`, no parsing will be done.
	* If `hooks` is `"parser"`, the values will be parsed as JSON.
	* If `hooks` is an object, the values will be parsed by the hooks.
	*/
	constructor(keyPref, valuePrefPrefix, hooks = "default") {
		super();
		this.keyPref = keyPref;
		this.valuePrefPrefix = valuePrefPrefix;
		if (hooks === "default") this.hooks = defaultHooks;
		else if (hooks === "parser") this.hooks = parserHooks;
		else this.hooks = {
			...defaultHooks,
			...hooks
		};
		this.innerObj = {};
	}
	/**
	* Get the object that stores the data.
	* @returns The object that stores the data.
	*/
	asObject() {
		return this.constructTempObj();
	}
	/**
	* Get the Map that stores the data.
	* @returns The Map that stores the data.
	*/
	asMapLike() {
		const mapLike = {
			get: (key) => this.getValue(key),
			set: (key, value) => {
				this.setValue(key, value);
				return mapLike;
			},
			has: (key) => this.hasKey(key),
			delete: (key) => this.deleteKey(key),
			clear: () => {
				for (const key of this.getKeys()) this.deleteKey(key);
			},
			forEach: (callback) => {
				return this.constructTempMap().forEach(callback);
			},
			get size() {
				return this._this.getKeys().length;
			},
			entries: () => {
				return this.constructTempMap().values();
			},
			keys: () => {
				const keys = this.getKeys();
				return keys[Symbol.iterator]();
			},
			values: () => {
				return this.constructTempMap().values();
			},
			[Symbol.iterator]: () => {
				return this.constructTempMap()[Symbol.iterator]();
			},
			[Symbol.toStringTag]: "MapLike",
			_this: this
		};
		return mapLike;
	}
	/**
	* Get the keys of the data.
	* @returns The keys of the data.
	*/
	getKeys() {
		const rawKeys = Zotero.Prefs.get(this.keyPref, true);
		const keys = rawKeys ? JSON.parse(rawKeys) : [];
		for (const key of keys) {
			const value = "placeholder";
			this.innerObj[key] = value;
		}
		return keys;
	}
	/**
	* Set the keys of the data.
	* @param keys The keys of the data.
	*/
	setKeys(keys) {
		keys = [...new Set(keys.filter((key) => key))];
		Zotero.Prefs.set(this.keyPref, JSON.stringify(keys), true);
		for (const key of keys) {
			const value = "placeholder";
			this.innerObj[key] = value;
		}
	}
	/**
	* Get the value of a key.
	* @param key The key of the data.
	* @returns The value of the key.
	*/
	getValue(key) {
		const value = Zotero.Prefs.get(`${this.valuePrefPrefix}${key}`, true);
		if (typeof value === "undefined") return;
		const { value: newValue } = this.hooks.afterGetValue({ value });
		this.innerObj[key] = newValue;
		return newValue;
	}
	/**
	* Set the value of a key.
	* @param key The key of the data.
	* @param value The value of the key.
	*/
	setValue(key, value) {
		const { key: newKey, value: newValue } = this.hooks.beforeSetValue({
			key,
			value
		});
		this.setKey(newKey);
		Zotero.Prefs.set(`${this.valuePrefPrefix}${newKey}`, newValue, true);
		this.innerObj[newKey] = newValue;
	}
	/**
	* Check if a key exists.
	* @param key The key of the data.
	* @returns Whether the key exists.
	*/
	hasKey(key) {
		return this.getKeys().includes(key);
	}
	/**
	* Add a key.
	* @param key The key of the data.
	*/
	setKey(key) {
		const keys = this.getKeys();
		if (!keys.includes(key)) {
			keys.push(key);
			this.setKeys(keys);
		}
	}
	/**
	* Delete a key.
	* @param key The key of the data.
	*/
	deleteKey(key) {
		const keys = this.getKeys();
		const index = keys.indexOf(key);
		if (index > -1) {
			keys.splice(index, 1);
			delete this.innerObj[key];
			this.setKeys(keys);
		}
		Zotero.Prefs.clear(`${this.valuePrefPrefix}${key}`, true);
		return true;
	}
	constructTempObj() {
		return new Proxy(this.innerObj, {
			get: (target, prop, receiver) => {
				this.getKeys();
				if (typeof prop === "string" && prop in target) this.getValue(prop);
				return Reflect.get(target, prop, receiver);
			},
			set: (target, p, newValue, receiver) => {
				if (typeof p === "string") {
					if (newValue === void 0) {
						this.deleteKey(p);
						return true;
					}
					this.setValue(p, newValue);
					return true;
				}
				return Reflect.set(target, p, newValue, receiver);
			},
			has: (target, p) => {
				this.getKeys();
				return Reflect.has(target, p);
			},
			deleteProperty: (target, p) => {
				if (typeof p === "string") {
					this.deleteKey(p);
					return true;
				}
				return Reflect.deleteProperty(target, p);
			}
		});
	}
	constructTempMap() {
		const map = /* @__PURE__ */ new Map();
		for (const key of this.getKeys()) map.set(key, this.getValue(key));
		return map;
	}
};
const defaultHooks = {
	afterGetValue: ({ value }) => ({ value }),
	beforeSetValue: ({ key, value }) => ({
		key,
		value
	})
};
const parserHooks = {
	afterGetValue: ({ value }) => {
		try {
			value = JSON.parse(value);
		} catch {
			return { value };
		}
		return { value };
	},
	beforeSetValue: ({ key, value }) => {
		value = JSON.stringify(value);
		return {
			key,
			value
		};
	}
};

//#endregion
//#region src/helpers/message.ts
function randomJobID() {
	return `${Math.random().toString(36).substring(2, 15)}-${Date.now()}`;
}
function getProperty(obj, path) {
	if (path === "") return obj;
	const parts = path.split(".");
	let value = obj;
	for (const part of parts) {
		value = value[part];
		if (typeof value === "undefined") throw new TypeError(`Property ${path} not found`);
	}
	return value;
}
function logError(e) {
	if (typeof self !== "undefined") self.console.warn(e);
	else if (typeof console !== "undefined") console.warn(e);
	else if (typeof Zotero !== "undefined") Zotero.warn(e);
}
/**
* Helper class to manage messages between workers/iframes and their parent
* @beta
*
* @example
* Use the `MessageHelper` to create a server that can be used to communicate between workers or iframes and their parent.
*
* In the child `worker.js`:
* ```typescript
* const handlers = {
*   async test() {
*     return "test";
*   },
* };
* // Create a new server
* const server = new MessageHelper({
*   name: "child",
*   handlers,
*   canBeDestroyed: true,
* });
* // Start the listener
* server.start();
* // Export the handlers for type hinting
* export { handlers };
* ```
* In the parent:
* ```typescript
* // Import the handlers
* import type { handlers } from "./worker.js";
* // Create a new worker
* const worker = new Worker("worker.js");
* // Create a new server with the type from the target handlers
* const server = new MessageHelper<typeof handlers>({
*   name: "worker",
*   handlers: {
*     async test() {
*       return "test";
*     },
*   },
*   target: worker,
* });
* server.start();
* // Execute the handlers defined in the worker as if they were local
* ztoolkit.log(await server.proxy.test());
* // ...
* // Stop the server, can be restarted with server.start()
* server.stop();
* // Destroy the server and the worker
* server.destroy();
* ```
*
* @example
* Evaluate code in the other side of the server
* ```typescript
* await server.eval("self.firstName = 'John';");
* ```
*
* @example
* Get a property from the other side of the server, can be nested.
*
* Only works if the property is a primitive or a serializable object
* ```typescript
* ztoolkit.log(await server.get("self.firstName"));
* ```
*
* @example
* Set a property from the other side of the server, can be nested.
*
* Only works if the property is a primitive or a serializable object
* ```typescript
* await server.set("self.firstName", "Alice");
* ```
*
* @example
* Check if the target is alive
* ```typescript
* ztoolkit.log(await server.isTargetAlive());
* // Alternatively, send a ping message
* ztoolkit.log(await server.proxy._ping());
* ```
*/
var MessageHelper = class {
	config;
	env;
	listener;
	running = false;
	/**
	* Proxy object to call the message handlers
	*/
	proxy = new Proxy({}, { get: (target, prop) => {
		return async (...args) => {
			return await this.exec(prop, args);
		};
	} });
	get target() {
		return this.config.target;
	}
	get privileged() {
		return ["browser", "chromeworker"].includes(this.env);
	}
	constructor(config) {
		if (!config.target) config.target = self;
		if (typeof config.dev === "undefined") config.dev = false;
		this.config = config;
		this.config.handlers = {
			...this.config.handlers,
			_start: async () => {
				this.start();
			},
			_stop: async () => {
				this.stop();
			},
			_destroy: async () => {
				this.destroy();
			},
			_ping: async () => {
				return "pong";
			},
			_call: async (func, args) => {
				const funcObj = getProperty(self, func);
				return await funcObj(...args);
			},
			_get: async (key) => {
				return await getProperty(self, key);
			},
			_set: async (key, value) => {
				const parts = key.split(".");
				const last = parts.pop();
				const obj = getProperty(self, parts.join("."));
				obj[last] = value;
			},
			_eval: async (code) => {
				const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
				const fn = new AsyncFunction("self", code);
				return await fn(self);
			}
		};
		this.config.target.addEventListener("unload", () => {
			this.destroy();
		}, { once: true });
		if (typeof window !== "undefined") if (typeof window.browsingContext !== "undefined") this.env = "browser";
		else this.env = "content";
		else if (typeof ctypes !== "undefined") this.env = "chromeworker";
		else this.env = "webworker";
	}
	start() {
		if (this.running) {
			logError(`Listener already started for ${this.config.name}`);
			return;
		}
		if (!this.listener) this.listener = async (event) => {
			const { data } = event;
			if (typeof data !== "object" || data === null) return;
			const { _jobID, _senderName, _handlerName, _handlerData, _requestReturn } = data;
			if (_senderName === this.config.name) return;
			if (!_handlerName) return;
			if (!this.running && _handlerName !== "_start") {
				logError(`Server not started for ${this.config.name}, ignoring message ${_handlerName}`);
				return;
			}
			const handler = this.config.handlers[_handlerName];
			if (!handler) {
				if (!_handlerName.endsWith("::return")) logError(`Handler ${_handlerName} not found for ${this.config.name}`);
				return;
			}
			try {
				const res = await handler(..._handlerData || []);
				if (_requestReturn) this.send({
					name: `${_handlerName}::return`,
					data: res,
					jobID: _jobID,
					requestReturn: false
				});
			} catch (e) {
				logError(`Error in handler ${_handlerName} for ${this.config.name}, ${e}`);
				if (_requestReturn) this.send({
					name: `${_handlerName}::return`,
					data: String(e),
					jobID: _jobID,
					success: false,
					requestReturn: false
				});
			}
		};
		this.config.target?.addEventListener("message", this.listener);
		this.running = true;
	}
	stop() {
		if (!this.running) {
			logError(`Listener already stopped for ${this.config.name}`);
			return;
		}
		this.running = false;
	}
	destroy() {
		this.stop();
		if (!this.listener) {
			logError(`Listener already destroyed for ${this.config.name}`);
			return;
		}
		this.config.target?.removeEventListener("message", this.listener);
		if (this.config.canBeDestroyed && this.config.target === self) {
			if (this.config.dev) logError(`Destroying window ${this.config.name}`);
			self.close();
		}
	}
	async exec(name, params, options = {}) {
		const { timeout = 5e3 } = options;
		let resolved = false;
		const target = this.config.target;
		const jobID = await this.send({
			name,
			data: params || []
		});
		return new Promise((resolve) => {
			const handler = function(event) {
				if (resolved) return;
				const { _handlerName, _jobID: returnJobID, _handlerData } = event.data;
				if (_handlerName !== `${name}::return` || returnJobID !== jobID) return;
				resolved = true;
				clearTimeout(timer);
				target.removeEventListener("message", handler);
				resolve(_handlerData);
			};
			target.addEventListener("message", handler);
			const timer = setTimeout(() => {
				if (resolved) return;
				logError(`Timeout for ${name} in ${this.config.name}`);
				resolved = true;
				target.removeEventListener("message", handler);
				resolve(void 0);
			}, timeout);
		});
	}
	async call(func, args) {
		return await this.exec("_call", [func, args]);
	}
	async get(key) {
		return await this.exec("_get", [key]);
	}
	async set(key, value) {
		return await this.exec("_set", [key, value]);
	}
	async eval(code) {
		return await this.exec("_eval", [code]);
	}
	async send(options) {
		const { name, data: params } = options;
		let { jobID, success, requestReturn } = options;
		if (!jobID) jobID = randomJobID();
		if (typeof success === "undefined") success = true;
		if (typeof requestReturn === "undefined") requestReturn = true;
		if (this.config.dev) logError(`Sending message ${name} from ${this.config.name}, ${(/* @__PURE__ */ new Date()).toISOString()}`);
		const message = {
			_jobID: jobID,
			_senderName: this.config.name,
			_handlerName: name,
			_requestReturn: requestReturn,
			_handlerData: params
		};
		this.config.target.postMessage(message);
		return jobID;
	}
	async isTargetAlive() {
		if (!this.target) return false;
		if (typeof this.target.closed !== "undefined") return !this.target.closed;
		else {
			const ret = await this.exec("_ping", void 0, { timeout: 200 });
			return ret === "pong";
		}
	}
};

//#endregion
//#region src/helpers/patch.ts
var PatchHelper = class extends BasicTool {
	options;
	constructor() {
		super();
		this.options = void 0;
	}
	setData(options) {
		this.options = options;
		const Zotero$1 = this.getGlobal("Zotero");
		const { target, funcSign, patcher } = options;
		const origin = target[funcSign];
		this.log("patching ", funcSign);
		target[funcSign] = function(...args) {
			if (options.enabled) try {
				return patcher(origin).apply(this, args);
			} catch (e) {
				Zotero$1.logError(e);
			}
			return origin.apply(this, args);
		};
		return this;
	}
	enable() {
		if (!this.options) throw new Error("No patch data set");
		this.options.enabled = true;
		return this;
	}
	disable() {
		if (!this.options) throw new Error("No patch data set");
		this.options.enabled = false;
		return this;
	}
};

//#endregion
//#region src/helpers/progressWindow.ts
/**
* Icon dict. Add your custom icons here.
* @default
* ```ts
* {
*   success: "chrome://zotero/skin/tick.png",
*   fail: "chrome://zotero/skin/cross.png",
* };
* ```
*/
const icons = {
	success: "chrome://zotero/skin/tick.png",
	fail: "chrome://zotero/skin/cross.png"
};
/**
* ProgressWindow helper.
* @example
* Show a popup with success icon
* ```ts
* const tool = new ZoteroTool();
* tool.createProgressWindow("Addon").createLine({
*   type: "success",
*   text: "Finish"
*   progress: 100,
* }).show();
* ```
* @example
* Show a popup and change line content
* ```ts
* const compat = new ZoteroCompat();
* const tool = new ZoteroTool();
* const popupWin = tool.createProgressWindow("Addon").createLine({
*   text: "Loading"
*   progress: 50,
* }).show(-1);
* // Do operations
* compat.getGlobal("setTimeout")(()=>{
*   popupWin.changeLine({
*     text: "Finish",
*     progress: 100,
*   });
* }, 3000);
* ```
*/
var ProgressWindowHelper = class {
	win;
	lines;
	closeTime;
	/**
	*
	* @param header window header
	* @param options
	* @param options.window
	* @param options.closeOnClick
	* @param options.closeTime
	* @param options.closeOtherProgressWindows
	*/
	constructor(header, options = {
		closeOnClick: true,
		closeTime: 5e3
	}) {
		this.win = new (BasicTool.getZotero()).ProgressWindow(options);
		this.lines = [];
		this.closeTime = options.closeTime || 5e3;
		this.win.changeHeadline(header);
		if (options.closeOtherProgressWindows) BasicTool.getZotero().ProgressWindowSet.closeAll();
	}
	/**
	* Create a new line
	* @param options
	* @param options.type
	* @param options.icon
	* @param options.text
	* @param options.progress
	* @param options.idx
	*/
	createLine(options) {
		const icon = this.getIcon(options.type, options.icon);
		const line = new this.win.ItemProgress(icon || "", options.text || "");
		if (typeof options.progress === "number") line.setProgress(options.progress);
		this.lines.push(line);
		this.updateIcons();
		return this;
	}
	/**
	* Change the line content
	* @param options
	* @param options.type
	* @param options.icon
	* @param options.text
	* @param options.progress
	* @param options.idx
	*/
	changeLine(options) {
		if (this.lines?.length === 0) return this;
		const idx = typeof options.idx !== "undefined" && options.idx >= 0 && options.idx < this.lines.length ? options.idx : 0;
		const icon = this.getIcon(options.type, options.icon);
		if (icon) this.lines[idx].setItemTypeAndIcon(icon);
		options.text && this.lines[idx].setText(options.text);
		typeof options.progress === "number" && this.lines[idx].setProgress(options.progress);
		this.updateIcons();
		return this;
	}
	show(closeTime = void 0) {
		this.win.show();
		typeof closeTime !== "undefined" && (this.closeTime = closeTime);
		if (this.closeTime && this.closeTime > 0) this.win.startCloseTimer(this.closeTime);
		setTimeout(this.updateIcons.bind(this), 50);
		return this;
	}
	/**
	* Set custom icon uri for progress window
	* @param key
	* @param uri
	*/
	static setIconURI(key, uri) {
		icons[key] = uri;
	}
	getIcon(type, defaultIcon) {
		return type && type in icons ? icons[type] : defaultIcon;
	}
	updateIcons() {
		try {
			this.lines.forEach((line) => {
				const box = line._image;
				const icon = box.dataset.itemType;
				if (icon && !box.style.backgroundImage.includes("progress_arcs")) box.style.backgroundImage = `url(${box.dataset.itemType})`;
			});
		} catch {}
	}
	changeHeadline(text, icon, postText) {
		this.win.changeHeadline(text, icon, postText);
		return this;
	}
	addLines(labels, icons$1) {
		this.win.addLines(labels, icons$1);
		return this;
	}
	addDescription(text) {
		this.win.addDescription(text);
		return this;
	}
	startCloseTimer(ms, requireMouseOver) {
		this.win.startCloseTimer(ms, requireMouseOver);
		return this;
	}
	close() {
		this.win.close();
		return this;
	}
};

//#endregion
//#region src/helpers/settingsDialog.ts
/**
* Settings dialog helper. Extends DialogHelper with setting management capabilities.
*/
var SettingsDialogHelper = class extends DialogHelper {
	settingsHandlers = null;
	autoSaveButtonIds = /* @__PURE__ */ new Set();
	settingBindings = /* @__PURE__ */ new Map();
	/**
	* Create a settings dialog helper.
	* Uses a 2-column grid layout by default (label column + control column)
	*/
	constructor() {
		super(1, 2);
		this.elementProps = {
			tag: "vbox",
			attributes: { flex: 1 },
			styles: {
				width: "100%",
				height: "100%",
				padding: "20px"
			},
			children: [{
				tag: "div",
				classList: ["settings-grid"],
				styles: {
					display: "grid",
					gridTemplateColumns: "auto 1fr",
					gap: "15px 20px",
					alignItems: "center",
					marginBottom: "20px"
				},
				children: []
			}, {
				tag: "hbox",
				attributes: {
					flex: 0,
					pack: "end"
				},
				styles: { marginTop: "20px" },
				children: []
			}]
		};
	}
	/**
	* Set the setting handlers for getting and setting values.
	* @param getSetting Function to get a setting value by key
	* @param setSetting Function to set a setting value by key
	*/
	setSettingHandlers(getSetting, setSetting) {
		this.settingsHandlers = {
			getSetting,
			setSetting
		};
		return this;
	}
	/**
	* Add a setting row with label and form control.
	* @param label Label text for the setting
	* @param settingKey The key used to store/retrieve the setting
	* @param controlProps Properties for the form control element
	* @param options Additional options
	* @param options.valueType Type of the setting value for proper conversion
	* @param options.labelProps Properties for the label element
	* @param options.condition Optional condition function to determine if the setting should be added
	*                          (returns true to add, false to skip)
	* @returns The SettingsDialogHelper instance for chaining
	*/
	addSetting(label, settingKey, controlProps, options = {}) {
		const { valueType = "string", labelProps = {}, condition } = options;
		if (condition && !condition()) return this;
		const controlId = `setting-${settingKey}-${Zotero.Utilities.randomString()}`;
		this.settingBindings.set(controlId, {
			key: settingKey,
			valueType
		});
		const gridContainer = this.elementProps.children[0];
		const labelElement = {
			tag: "label",
			attributes: { for: controlId },
			properties: { textContent: label },
			styles: {
				fontWeight: "500",
				textAlign: "right",
				paddingRight: "10px"
			},
			...labelProps
		};
		const controlElement = {
			...controlProps,
			id: controlId,
			attributes: {
				...controlProps.attributes,
				"data-setting-key": settingKey,
				"data-setting-type": valueType
			}
		};
		if (this.settingsHandlers) {
			const currentValue = this.settingsHandlers.getSetting(settingKey);
			this.setControlValue(controlElement, currentValue, valueType);
		}
		gridContainer.children.push(labelElement, controlElement);
		return this;
	}
	/**
	* Add a static row (label + static element) to the settings grid. This is not a form control.
	* @param label Label text for the row
	* @param staticElementProps Properties for the static element (e.g., text, icon, etc.)
	* @param options Additional options
	* @param options.labelProps Properties for the label element
	* @param options.condition Optional condition function to determine if the row should be added
	* @returns The SettingsDialogHelper instance for chaining
	*/
	addStaticRow(label, staticElementProps, options = {}) {
		const { labelProps = {}, condition } = options;
		if (condition && !condition()) return this;
		const gridContainer = this.elementProps.children[0];
		const labelElement = {
			tag: "label",
			properties: { textContent: label },
			styles: {
				fontWeight: "500",
				textAlign: "right",
				paddingRight: "10px"
			},
			...labelProps
		};
		const staticElement = {
			...staticElementProps,
			attributes: { ...staticElementProps.attributes }
		};
		gridContainer.children.push(labelElement, staticElement);
		return this;
	}
	/**
	* Add a control button that will auto-save settings when clicked.
	* @param label Button label
	* @param id Button id
	* @param options Button options
	* @param options.noClose Don't close window when clicking this button
	* @param options.validate Validation function for settings data
	* @param options.callback Callback of button click event
	*/
	addAutoSaveButton(label, id, options = {}) {
		id = id || `btn-${Zotero.Utilities.randomString()}-${(/* @__PURE__ */ new Date()).getTime()}`;
		this.autoSaveButtonIds.add(id);
		return this.addButton(label, id, {
			...Object.assign({}, options, { noClose: true }),
			callback: async (ev) => {
				if (options.validate) {
					const data = this.getAllSettingsData();
					const validationResult = await options.validate(data);
					if (validationResult !== true) {
						this.window.alert(validationResult);
						return;
					}
				}
				this.saveAllSettings();
				if (options.callback) options.callback(ev);
				if (!options.noClose) this.window.close();
			}
		});
	}
	/**
	* Save all settings using the setting handlers.
	*/
	saveAllSettings() {
		if (!this.settingsHandlers) {
			console.warn("SettingsDialog: No setting handlers configured");
			return;
		}
		const settingsData = this.getAllSettingsData();
		Object.entries(settingsData).forEach(([key, value]) => {
			this.settingsHandlers.setSetting(key, value);
		});
	}
	/**
	* Collect and return all current setting values from the dialog controls.
	*/
	getAllSettingsData() {
		const data = {};
		if (this.window) {
			const settingControls = this.window.document.querySelectorAll("[data-setting-key]");
			settingControls.forEach((control) => {
				const settingKey = control.getAttribute("data-setting-key");
				const valueType = control.getAttribute("data-setting-type");
				if (settingKey) data[settingKey] = this.getControlValue(control, valueType);
			});
		}
		return data;
	}
	/**
	* Load all settings from the setting handlers.
	*/
	loadAllSettings() {
		if (!this.settingsHandlers || !this.window) return;
		const settingControls = this.window.document.querySelectorAll("[data-setting-key]");
		settingControls.forEach((control) => {
			const settingKey = control.getAttribute("data-setting-key");
			const valueType = control.getAttribute("data-setting-type");
			if (settingKey) {
				const value = this.settingsHandlers.getSetting(settingKey);
				this.setControlValueOnElement(control, value, valueType);
			}
		});
	}
	/**
	* Override the open method to handle setting loading after window opens.
	*/
	open(title, windowFeatures = {
		centerscreen: true,
		resizable: true,
		fitContent: true
	}) {
		const originalLoadCallback = this.dialogData.loadCallback;
		this.dialogData.loadCallback = () => {
			this.loadAllSettings();
			this.window.document.body.style.overflow = "hidden";
			if (windowFeatures.fitContent) this.window.sizeToContent();
			if (originalLoadCallback) originalLoadCallback();
		};
		const originalBeforeUnloadCallback = this.dialogData.beforeUnloadCallback;
		this.dialogData.beforeUnloadCallback = () => {
			if (this.dialogData._lastButtonId && this.autoSaveButtonIds.has(this.dialogData._lastButtonId)) this.saveAllSettings();
			if (originalBeforeUnloadCallback) originalBeforeUnloadCallback();
		};
		return super.open(title, windowFeatures);
	}
	/**
	* Set control value based on element type and value type.
	*/
	setControlValue(element, value, _valueType) {
		if (value === void 0 || value === null) return;
		switch (element.tag) {
			case "input": {
				const inputType = element.attributes?.type || "text";
				if (inputType === "checkbox" || inputType === "radio") element.attributes = {
					...element.attributes,
					checked: Boolean(value)
				};
				else element.attributes = {
					...element.attributes,
					value: String(value)
				};
				break;
			}
			case "select":
				element.attributes = {
					...element.attributes,
					value: String(value)
				};
				break;
			case "textarea":
				element.properties = {
					...element.properties,
					value: String(value)
				};
				break;
			default: element.properties = {
				...element.properties,
				textContent: String(value)
			};
		}
	}
	/**
	* Set control value on an actual DOM element.
	*/
	setControlValueOnElement(element, value, _valueType) {
		if (value === void 0 || value === null) return;
		const tagName = element.tagName.toLowerCase();
		switch (tagName) {
			case "input": {
				const inputElement = element;
				if (inputElement.type === "checkbox" || inputElement.type === "radio") inputElement.checked = Boolean(value);
				else inputElement.value = String(value);
				break;
			}
			case "select":
				element.value = String(value);
				break;
			case "textarea":
				element.value = String(value);
				break;
			default: element.textContent = String(value);
		}
	}
	/**
	* Get control value from a DOM element with proper type conversion.
	*/
	getControlValue(element, valueType) {
		const tagName = element.tagName.toLowerCase();
		let rawValue;
		switch (tagName) {
			case "input": {
				const inputElement = element;
				if (inputElement.type === "checkbox" || inputElement.type === "radio") rawValue = inputElement.checked;
				else rawValue = inputElement.value;
				break;
			}
			case "select":
				rawValue = element.value;
				break;
			case "textarea":
				rawValue = element.value;
				break;
			default: rawValue = element.textContent || "";
		}
		switch (valueType) {
			case "number": return typeof rawValue === "string" ? Number(rawValue) : rawValue;
			case "boolean": return typeof rawValue === "boolean" ? rawValue : Boolean(rawValue);
			case "string":
			default: return String(rawValue);
		}
	}
};

//#endregion
//#region src/helpers/virtualizedTable.ts
/**
* VirtualizedTable helper.
*/
var VirtualizedTableHelper = class extends BasicTool {
	props;
	localeStrings;
	containerId;
	treeInstance;
	window;
	React;
	ReactDOM;
	VirtualizedTable;
	IntlProvider;
	constructor(win) {
		super();
		this.window = win;
		const Zotero$1 = this.getGlobal("Zotero");
		const _require = win.require;
		this.React = _require("react");
		this.ReactDOM = _require("react-dom");
		this.VirtualizedTable = _require("components/virtualized-table");
		this.IntlProvider = _require("react-intl").IntlProvider;
		this.props = {
			id: `vtable-${Zotero$1.Utilities.randomString()}-${(/* @__PURE__ */ new Date()).getTime()}`,
			getRowCount: () => 0
		};
		this.localeStrings = Zotero$1.Intl.strings;
	}
	setProp(...args) {
		if (args.length === 1) Object.assign(this.props, args[0]);
		else if (args.length === 2) this.props[args[0]] = args[1];
		return this;
	}
	/**
	* Set locale strings, which replaces the table header's label if matches. Default it's `Zotero.Intl.strings`
	* @param localeStrings
	*/
	setLocale(localeStrings) {
		Object.assign(this.localeStrings, localeStrings);
		return this;
	}
	/**
	* Set container element id that the table will be rendered on.
	* @param id element id
	*/
	setContainerId(id) {
		this.containerId = id;
		return this;
	}
	/**
	* Render the table.
	* @param selectId Which row to select after rendering
	* @param onfulfilled callback after successfully rendered
	* @param onrejected callback after rendering with error
	*/
	render(selectId, onfulfilled, onrejected) {
		const refreshSelection = () => {
			this.treeInstance.invalidate();
			if (typeof selectId !== "undefined" && selectId >= 0) this.treeInstance.selection.select(selectId);
			else this.treeInstance.selection.clearSelection();
		};
		if (!this.treeInstance) new Promise((resolve) => {
			const vtableProps = Object.assign({}, this.props, { ref: (ref) => {
				this.treeInstance = ref;
				resolve(void 0);
			} });
			if (vtableProps.getRowData && !vtableProps.renderItem) Object.assign(vtableProps, { renderItem: this.VirtualizedTable.makeRowRenderer(vtableProps.getRowData) });
			const elem = this.React.createElement(this.IntlProvider, {
				locale: Zotero.locale,
				messages: Zotero.Intl.strings
			}, this.React.createElement(this.VirtualizedTable, vtableProps));
			const container = this.window.document.getElementById(this.containerId);
			this.ReactDOM.createRoot(container).render(elem);
		}).then(() => {
			this.getGlobal("setTimeout")(() => {
				refreshSelection();
			});
		}).then(onfulfilled, onrejected);
		else refreshSelection();
		return this;
	}
};

//#endregion
//#region src/managers/fieldHook.ts
/**
* Item field hooks manager.
*/
var FieldHookManager = class extends ManagerTool {
	data = {
		getField: {},
		setField: {},
		isFieldOfBase: {}
	};
	patchHelpers = {
		getField: new PatchHelper(),
		setField: new PatchHelper(),
		isFieldOfBase: new PatchHelper()
	};
	constructor(base) {
		super(base);
		const _thisHelper = this;
		for (const type of Object.keys(this.patchHelpers)) {
			const helper = this.patchHelpers[type];
			helper.setData({
				target: this.getGlobal("Zotero").Item.prototype,
				funcSign: type,
				patcher: (original) => function(field, ...args) {
					const originalThis = this;
					const handler = _thisHelper.data[type][field];
					if (typeof handler === "function") try {
						return handler(field, args[0], args[1], originalThis, original);
					} catch (e) {
						return field + String(e);
					}
					return original.apply(originalThis, [field, ...args]);
				},
				enabled: true
			});
		}
	}
	register(type, field, hook) {
		this.data[type][field] = hook;
	}
	unregister(type, field) {
		delete this.data[type][field];
	}
	unregisterAll() {
		this.data.getField = {};
		this.data.setField = {};
		this.data.isFieldOfBase = {};
		this.patchHelpers.getField.disable();
		this.patchHelpers.setField.disable();
		this.patchHelpers.isFieldOfBase.disable();
	}
};

//#endregion
//#region src/utils/wait.ts
var wait_exports = {};
__export(wait_exports, {
	waitForReader: () => waitForReader,
	waitUntil: () => waitUntil,
	waitUntilAsync: () => waitUntilAsync,
	waitUtilAsync: () => waitUtilAsync
});
const basicTool = new BasicTool();
function waitUntil(condition, callback, interval = 100, timeout = 1e4) {
	const start = Date.now();
	const intervalId = basicTool.getGlobal("setInterval")(() => {
		if (condition()) {
			basicTool.getGlobal("clearInterval")(intervalId);
			callback();
		} else if (Date.now() - start > timeout) basicTool.getGlobal("clearInterval")(intervalId);
	}, interval);
}
const waitUtilAsync = waitUntilAsync;
function waitUntilAsync(condition, interval = 100, timeout = 1e4) {
	return new Promise((resolve, reject) => {
		const start = Date.now();
		const intervalId = basicTool.getGlobal("setInterval")(() => {
			if (condition()) {
				basicTool.getGlobal("clearInterval")(intervalId);
				resolve();
			} else if (Date.now() - start > timeout) {
				basicTool.getGlobal("clearInterval")(intervalId);
				reject(/* @__PURE__ */ new Error("timeout"));
			}
		}, interval);
	});
}
async function waitForReader(reader) {
	await reader._initPromise;
	await reader._lastView.initializedPromise;
	if (reader.type === "pdf") await reader._lastView._iframeWindow.PDFViewerApplication.initializedPromise;
}

//#endregion
//#region src/managers/keyboard.ts
/**
* Register a global keyboard event listener.
*/
var KeyboardManager = class extends ManagerTool {
	_keyboardCallbacks = /* @__PURE__ */ new Set();
	_cachedKey;
	id;
	constructor(base) {
		super(base);
		this.id = `kbd-${Zotero.Utilities.randomString()}`;
		this._ensureAutoUnregisterAll();
		this.addListenerCallback("onMainWindowLoad", this.initKeyboardListener);
		this.addListenerCallback("onMainWindowUnload", this.unInitKeyboardListener);
		this.initReaderKeyboardListener();
		for (const win of Zotero.getMainWindows()) this.initKeyboardListener(win);
	}
	/**
	* Register a keyboard event listener.
	* @param callback The callback function.
	*/
	register(callback) {
		this._keyboardCallbacks.add(callback);
	}
	/**
	* Unregister a keyboard event listener.
	* @param callback The callback function.
	*/
	unregister(callback) {
		this._keyboardCallbacks.delete(callback);
	}
	/**
	* Unregister all keyboard event listeners.
	*/
	unregisterAll() {
		this._keyboardCallbacks.clear();
		this.removeListenerCallback("onMainWindowLoad", this.initKeyboardListener);
		this.removeListenerCallback("onMainWindowUnload", this.unInitKeyboardListener);
		for (const win of Zotero.getMainWindows()) this.unInitKeyboardListener(win);
	}
	initKeyboardListener = this._initKeyboardListener.bind(this);
	unInitKeyboardListener = this._unInitKeyboardListener.bind(this);
	initReaderKeyboardListener() {
		Zotero.Reader.registerEventListener("renderToolbar", (event) => this.addReaderKeyboardCallback(event), this._basicOptions.api.pluginID);
		Zotero.Reader._readers.forEach((reader) => this.addReaderKeyboardCallback({ reader }));
	}
	async addReaderKeyboardCallback(event) {
		const reader = event.reader;
		const initializedKey = `_ztoolkitKeyboard${this.id}Initialized`;
		await waitForReader(reader);
		if (!reader._iframeWindow) return;
		if (reader._iframeWindow[initializedKey]) return;
		this._initKeyboardListener(reader._iframeWindow);
		waitUntil(() => !Components.utils.isDeadWrapper(reader._internalReader) && (reader._internalReader?._primaryView)?._iframeWindow, () => this._initKeyboardListener(reader._internalReader._primaryView?._iframeWindow));
		reader._iframeWindow[initializedKey] = true;
	}
	_initKeyboardListener(win) {
		if (!win) return;
		win.addEventListener("keydown", this.triggerKeydown);
		win.addEventListener("keyup", this.triggerKeyup);
	}
	_unInitKeyboardListener(win) {
		if (!win) return;
		win.removeEventListener("keydown", this.triggerKeydown);
		win.removeEventListener("keyup", this.triggerKeyup);
	}
	triggerKeydown = (e) => {
		if (!this._cachedKey) this._cachedKey = new KeyModifier(e);
		else this._cachedKey.merge(new KeyModifier(e), { allowOverwrite: false });
		this.dispatchCallback(e, { type: "keydown" });
	};
	triggerKeyup = async (e) => {
		if (!this._cachedKey) return;
		const currentShortcut = new KeyModifier(this._cachedKey);
		this._cachedKey = void 0;
		this.dispatchCallback(e, {
			keyboard: currentShortcut,
			type: "keyup"
		});
	};
	dispatchCallback(...args) {
		this._keyboardCallbacks.forEach((cbk) => cbk(...args));
	}
};
/**
* Class to represent key with modifiers
*/
var KeyModifier = class KeyModifier {
	accel = false;
	shift = false;
	control = false;
	meta = false;
	alt = false;
	key = "";
	useAccel = false;
	constructor(raw, options) {
		this.useAccel = options?.useAccel || false;
		if (typeof raw === "undefined") {} else if (typeof raw === "string") {
			raw = raw || "";
			raw = this.unLocalized(raw);
			this.accel = raw.includes("accel");
			this.shift = raw.includes("shift");
			this.control = raw.includes("control");
			this.meta = raw.includes("meta");
			this.alt = raw.includes("alt");
			this.key = raw.replace(/(accel|shift|control|meta|alt|[ ,\-])/g, "").toLocaleLowerCase();
		} else if (raw instanceof KeyModifier) this.merge(raw, { allowOverwrite: true });
		else {
			if (options?.useAccel) if (Zotero.isMac) this.accel = raw.metaKey;
			else this.accel = raw.ctrlKey;
			this.shift = raw.shiftKey;
			this.control = raw.ctrlKey;
			this.meta = raw.metaKey;
			this.alt = raw.altKey;
			if (![
				"Shift",
				"Meta",
				"Ctrl",
				"Alt",
				"Control"
			].includes(raw.key)) this.key = raw.key;
		}
	}
	/**
	* Merge another KeyModifier into this one.
	* @param newMod the new KeyModifier
	* @param options
	* @param options.allowOverwrite
	* @returns KeyModifier
	*/
	merge(newMod, options) {
		const allowOverwrite = options?.allowOverwrite || false;
		this.mergeAttribute("accel", newMod.accel, allowOverwrite);
		this.mergeAttribute("shift", newMod.shift, allowOverwrite);
		this.mergeAttribute("control", newMod.control, allowOverwrite);
		this.mergeAttribute("meta", newMod.meta, allowOverwrite);
		this.mergeAttribute("alt", newMod.alt, allowOverwrite);
		this.mergeAttribute("key", newMod.key, allowOverwrite);
		return this;
	}
	/**
	* Check if the current KeyModifier equals to another KeyModifier.
	* @param newMod the new KeyModifier
	* @returns true if equals
	*/
	equals(newMod) {
		if (typeof newMod === "string") newMod = new KeyModifier(newMod);
		if (this.shift !== newMod.shift || this.alt !== newMod.alt || this.key.toLowerCase() !== newMod.key.toLowerCase()) return false;
		if (this.accel || newMod.accel) {
			if (Zotero.isMac) {
				if ((this.accel || this.meta) !== (newMod.accel || newMod.meta) || this.control !== newMod.control) return false;
			} else if ((this.accel || this.control) !== (newMod.accel || newMod.control) || this.meta !== newMod.meta) return false;
		} else if (this.control !== newMod.control || this.meta !== newMod.meta) return false;
		return true;
	}
	/**
	* Get the raw string representation of the KeyModifier.
	*/
	getRaw() {
		const enabled = [];
		this.accel && enabled.push("accel");
		this.shift && enabled.push("shift");
		this.control && enabled.push("control");
		this.meta && enabled.push("meta");
		this.alt && enabled.push("alt");
		this.key && enabled.push(this.key);
		return enabled.join(",");
	}
	/**
	* Get the localized string representation of the KeyModifier.
	*/
	getLocalized() {
		const raw = this.getRaw();
		if (Zotero.isMac) return raw.replaceAll("control", "⌃").replaceAll("alt", "⌥").replaceAll("shift", "⇧").replaceAll("meta", "⌘");
		else return raw.replaceAll("control", "Ctrl").replaceAll("alt", "Alt").replaceAll("shift", "Shift").replaceAll("meta", "Win");
	}
	/**
	* Get the un-localized string representation of the KeyModifier.
	*/
	unLocalized(raw) {
		if (Zotero.isMac) return raw.replaceAll("⌃", "control").replaceAll("⌥", "alt").replaceAll("⇧", "shift").replaceAll("⌘", "meta");
		else return raw.replaceAll("Ctrl", "control").replaceAll("Alt", "alt").replaceAll("Shift", "shift").replaceAll("Win", "meta");
	}
	mergeAttribute(attribute, value, allowOverwrite) {
		if (allowOverwrite || !this[attribute]) this[attribute] = value;
	}
};

//#endregion
//#region src/managers/menu.ts
/**
* Register \<menuitem\>, \<menupopup\>, or \<menuseperator\> to Zotero right-click/window menus.
*/
var MenuManager = class extends ManagerTool {
	ui;
	constructor(base) {
		super(base);
		this.ui = new UITool(this);
	}
	/**
	* Insert an menu item/menu(with popup)/menuseprator into a menupopup
	* @remarks
	* options:
	* ```ts
	* export interface MenuitemOptions {
	*   tag: "menuitem" | "menu" | "menuseparator";
	*   id?: string;
	*   label?: string;
	*   // data url (chrome://xxx.png) or base64 url (data:image/png;base64,xxx)
	*   icon?: string;
	*   class?: string;
	*   styles?: { [key: string]: string };
	*   hidden?: boolean;
	*   disabled?: boolean;
	*   oncommand?: string;
	*   commandListener?: EventListenerOrEventListenerObject;
	*   // Attributes below are used when type === "menu"
	*   popupId?: string;
	*   onpopupshowing?: string;
	*   subElementOptions?: Array<MenuitemOptions>;
	* }
	* ```
	* @param menuPopup
	* @param options
	* @param insertPosition
	* @param anchorElement The menuitem will be put before/after `anchorElement`. If not set, put at start/end of the menupopup.
	* @example
	* Insert menuitem with icon into item menupopup
	* ```ts
	* // base64 or chrome:// url
	* const menuIcon = "chrome://addontemplate/content/icons/favicon@0.5x.png";
	* ztoolkit.Menu.register("item", {
	*   tag: "menuitem",
	*   id: "zotero-itemmenu-addontemplate-test",
	*   label: "Addon Template: Menuitem",
	*   oncommand: "alert('Hello World! Default Menuitem.')",
	*   icon: menuIcon,
	* });
	* ```
	* @example
	* Insert menu into file menupopup
	* ```ts
	* ztoolkit.Menu.register(
	*   "menuFile",
	*   {
	*     tag: "menu",
	*     label: "Addon Template: Menupopup",
	*     subElementOptions: [
	*       {
	*         tag: "menuitem",
	*         label: "Addon Template",
	*         oncommand: "alert('Hello World! Sub Menuitem.')",
	*       },
	*     ],
	*   },
	*   "before",
	*   Zotero.getMainWindow().document.querySelector(
	*     "#zotero-itemmenu-addontemplate-test"
	*   )
	* );
	* ```
	*/
	register(menuPopup, options, insertPosition = "after", anchorElement) {
		let popup;
		if (typeof menuPopup === "string") popup = this.getGlobal("document").querySelector(MenuSelector[menuPopup]);
		else popup = menuPopup;
		if (!popup) return false;
		const doc = popup.ownerDocument;
		const genMenuElement = (menuitemOption) => {
			const elementOption = {
				tag: menuitemOption.tag,
				id: menuitemOption.id,
				namespace: "xul",
				attributes: {
					label: menuitemOption.label || "",
					hidden: Boolean(menuitemOption.hidden),
					disabled: Boolean(menuitemOption.disabled),
					class: menuitemOption.class || "",
					oncommand: menuitemOption.oncommand || ""
				},
				classList: menuitemOption.classList,
				styles: menuitemOption.styles || {},
				listeners: [],
				children: []
			};
			if (menuitemOption.icon) {
				if (!this.getGlobal("Zotero").isMac) if (menuitemOption.tag === "menu") elementOption.attributes.class += " menu-iconic";
				else elementOption.attributes.class += " menuitem-iconic";
				elementOption.styles["list-style-image"] = `url(${menuitemOption.icon})`;
			}
			if (menuitemOption.commandListener) elementOption.listeners?.push({
				type: "command",
				listener: menuitemOption.commandListener
			});
			if (menuitemOption.tag === "menuitem") {
				elementOption.attributes.type = menuitemOption.type || "";
				elementOption.attributes.checked = menuitemOption.checked || false;
			}
			const menuItem = this.ui.createElement(doc, menuitemOption.tag, elementOption);
			if (menuitemOption.isHidden || menuitemOption.getVisibility) popup?.addEventListener("popupshowing", (ev) => {
				let hidden;
				if (menuitemOption.isHidden) hidden = menuitemOption.isHidden(menuItem, ev);
				else if (menuitemOption.getVisibility) {
					const visible = menuitemOption.getVisibility(menuItem, ev);
					hidden = typeof visible === "undefined" ? void 0 : !visible;
				}
				if (typeof hidden === "undefined") return;
				if (hidden) menuItem.setAttribute("hidden", "true");
				else menuItem.removeAttribute("hidden");
			});
			if (menuitemOption.isDisabled) popup?.addEventListener("popupshowing", (ev) => {
				const disabled = menuitemOption.isDisabled(menuItem, ev);
				if (typeof disabled === "undefined") return;
				if (disabled) menuItem.setAttribute("disabled", "true");
				else menuItem.removeAttribute("disabled");
			});
			if ((menuitemOption.tag === "menuitem" || menuitemOption.tag === "menuseparator") && menuitemOption.onShowing) popup?.addEventListener("popupshowing", (ev) => {
				menuitemOption.onShowing(menuItem, ev);
			});
			if (menuitemOption.tag === "menu") {
				const subPopup = this.ui.createElement(doc, "menupopup", {
					id: menuitemOption.popupId,
					attributes: { onpopupshowing: menuitemOption.onpopupshowing || "" }
				});
				menuitemOption.children?.forEach((childOption) => {
					subPopup.append(genMenuElement(childOption));
				});
				menuItem.append(subPopup);
			}
			return menuItem;
		};
		const topMenuItem = genMenuElement(options);
		if (popup.childElementCount) {
			if (!anchorElement) anchorElement = insertPosition === "after" ? popup.lastElementChild : popup.firstElementChild;
			anchorElement[insertPosition](topMenuItem);
		} else popup.appendChild(topMenuItem);
	}
	unregister(menuId) {
		this.getGlobal("document").querySelector(`#${menuId}`)?.remove();
	}
	unregisterAll() {
		this.ui.unregisterAll();
	}
};
var MenuSelector = /* @__PURE__ */ function(MenuSelector$1) {
	MenuSelector$1["menuFile"] = "#menu_FilePopup";
	MenuSelector$1["menuEdit"] = "#menu_EditPopup";
	MenuSelector$1["menuView"] = "#menu_viewPopup";
	MenuSelector$1["menuGo"] = "#menu_goPopup";
	MenuSelector$1["menuTools"] = "#menu_ToolsPopup";
	MenuSelector$1["menuHelp"] = "#menu_HelpPopup";
	MenuSelector$1["collection"] = "#zotero-collectionmenu";
	MenuSelector$1["item"] = "#zotero-itemmenu";
	return MenuSelector$1;
}(MenuSelector || {});

//#endregion
//#region src/managers/prompt.ts
/**
* Prompt for setting up or executing some commands quickly.
*
* `Shift + P` can show/hide its UI anywhere after registering commands.
*/
var Prompt = class {
	ui;
	base;
	get document() {
		return this.base.getGlobal("document");
	}
	/**
	* Record the last text entered
	*/
	lastInputText = "";
	/**
	* Default text
	*/
	defaultText = {
		placeholder: "Select a command...",
		empty: "No commands found."
	};
	/**
	* It controls the max line number of commands displayed in `commandsNode`.
	*/
	maxLineNum = 12;
	/**
	* It controls the max number of suggestions.
	*/
	maxSuggestionNum = 100;
	/**
	* The top-level HTML div node of `Prompt`
	*/
	promptNode;
	/**
	* The HTML input node of `Prompt`.
	*/
	inputNode;
	/**
	* Save all commands registered by all addons.
	*/
	commands = [];
	/**
	* Initialize `Prompt` but do not create UI.
	*/
	constructor() {
		this.base = new BasicTool();
		this.ui = new UITool();
		this.initializeUI();
	}
	/**
	* Initialize `Prompt` UI and then bind events on it.
	*/
	initializeUI() {
		this.addStyle();
		this.createHTML();
		this.initInputEvents();
		this.registerShortcut();
	}
	createHTML() {
		this.promptNode = this.ui.createElement(this.document, "div", {
			styles: { display: "none" },
			children: [{
				tag: "div",
				styles: {
					position: "fixed",
					left: "0",
					top: "0",
					backgroundColor: "transparent",
					width: "100%",
					height: "100%"
				},
				listeners: [{
					type: "click",
					listener: () => {
						this.promptNode.style.display = "none";
					}
				}]
			}]
		});
		this.promptNode.appendChild(this.ui.createElement(this.document, "div", {
			id: `zotero-plugin-toolkit-prompt`,
			classList: ["prompt-container"],
			children: [
				{
					tag: "div",
					classList: ["input-container"],
					children: [{
						tag: "input",
						classList: ["prompt-input"],
						attributes: {
							type: "text",
							placeholder: this.defaultText.placeholder
						}
					}, {
						tag: "div",
						classList: ["cta"]
					}]
				},
				{
					tag: "div",
					classList: ["commands-containers"]
				},
				{
					tag: "div",
					classList: ["instructions"],
					children: [
						{
							tag: "div",
							classList: ["instruction"],
							children: [{
								tag: "span",
								classList: ["key"],
								properties: { innerText: "↑↓" }
							}, {
								tag: "span",
								properties: { innerText: "to navigate" }
							}]
						},
						{
							tag: "div",
							classList: ["instruction"],
							children: [{
								tag: "span",
								classList: ["key"],
								properties: { innerText: "enter" }
							}, {
								tag: "span",
								properties: { innerText: "to trigger" }
							}]
						},
						{
							tag: "div",
							classList: ["instruction"],
							children: [{
								tag: "span",
								classList: ["key"],
								properties: { innerText: "esc" }
							}, {
								tag: "span",
								properties: { innerText: "to exit" }
							}]
						}
					]
				}
			]
		}));
		this.inputNode = this.promptNode.querySelector("input");
		this.document.documentElement.appendChild(this.promptNode);
	}
	/**
	* Show commands in a new `commandsContainer`
	* All other `commandsContainer` is hidden
	* @param commands Command[]
	* @param clear remove all `commandsContainer` if true
	*/
	showCommands(commands, clear = false) {
		if (clear) this.promptNode.querySelectorAll(".commands-container").forEach((e) => e.remove());
		this.inputNode.placeholder = this.defaultText.placeholder;
		const commandsContainer = this.createCommandsContainer();
		for (const command of commands) {
			/**
			* Filter out anonymous commands
			*/
			try {
				if (!command.name || command.when && !command.when()) continue;
			} catch {
				continue;
			}
			commandsContainer.appendChild(this.createCommandNode(command));
		}
	}
	/**
	* Create a `commandsContainer` div element, append to `commandsContainer` and hide others.
	* @returns commandsNode
	*/
	createCommandsContainer() {
		const commandsContainer = this.ui.createElement(this.document, "div", { classList: ["commands-container"] });
		this.promptNode.querySelectorAll(".commands-container").forEach((e) => {
			e.style.display = "none";
		});
		this.promptNode.querySelector(".commands-containers").appendChild(commandsContainer);
		return commandsContainer;
	}
	/**
	* Return current displayed `commandsContainer`
	* @returns
	*/
	getCommandsContainer() {
		return [...Array.from(this.promptNode.querySelectorAll(".commands-container"))].find((e) => {
			return e.style.display !== "none";
		});
	}
	/**
	* Create a command item for `Prompt` UI.
	* @param command
	* @returns
	*/
	createCommandNode(command) {
		const commandNode = this.ui.createElement(this.document, "div", {
			classList: ["command"],
			children: [{
				tag: "div",
				classList: ["content"],
				children: [{
					tag: "div",
					classList: ["name"],
					children: [{
						tag: "span",
						properties: { innerText: command.name }
					}]
				}, {
					tag: "div",
					classList: ["aux"],
					children: command.label ? [{
						tag: "span",
						classList: ["label"],
						properties: { innerText: command.label }
					}] : []
				}]
			}],
			listeners: [{
				type: "mousemove",
				listener: () => {
					this.selectItem(commandNode);
				}
			}, {
				type: "click",
				listener: async () => {
					await this.execCallback(command.callback);
				}
			}]
		});
		commandNode.command = command;
		return commandNode;
	}
	/**
	* Called when `enter` key is pressed.
	*/
	trigger() {
		[...Array.from(this.promptNode.querySelectorAll(".commands-container"))].find((e) => e.style.display !== "none").querySelector(".selected").click();
	}
	/**
	* Called when `escape` key is pressed.
	*/
	exit() {
		this.inputNode.placeholder = this.defaultText.placeholder;
		if (this.promptNode.querySelectorAll(".commands-containers .commands-container").length >= 2) {
			this.promptNode.querySelector(".commands-container:last-child").remove();
			const commandsContainer = this.promptNode.querySelector(".commands-container:last-child");
			commandsContainer.style.display = "";
			commandsContainer.querySelectorAll(".commands").forEach((e) => e.style.display = "flex");
			this.inputNode.focus();
		} else this.promptNode.style.display = "none";
	}
	async execCallback(callback) {
		if (Array.isArray(callback)) this.showCommands(callback);
		else await callback(this);
	}
	/**
	* Match suggestions for user's entered text.
	*/
	async showSuggestions(inputText) {
		const _w = /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-./:;<=>?@[\]^_`{|}~]/;
		const jw = /\s/;
		const Ww = /[\u0F00-\u0FFF\u3040-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\uFF66-\uFF9F]/;
		function Yw(e$1, t, n, i) {
			if (e$1.length === 0) return 0;
			let r = 0;
			r -= Math.max(0, e$1.length - 1), r -= i / 10;
			const o = e$1[0][0];
			return r -= (e$1[e$1.length - 1][1] - o + 1 - t) / 100, r -= o / 1e3, r -= n / 1e4;
		}
		function $w(e$1, t, n, i) {
			if (e$1.length === 0) return null;
			for (var r = n.toLowerCase(), o = 0, a = 0, s = [], l = 0; l < e$1.length; l++) {
				const c = e$1[l];
				const u = r.indexOf(c, a);
				if (u === -1) return null;
				const h = n.charAt(u);
				if (u > 0 && !_w.test(h) && !Ww.test(h)) {
					const p = n.charAt(u - 1);
					if (h.toLowerCase() !== h && p.toLowerCase() !== p || h.toUpperCase() !== h && !_w.test(p) && !jw.test(p) && !Ww.test(p)) if (i) {
						if (u !== a) {
							a += c.length, l--;
							continue;
						}
					} else o += 1;
				}
				if (s.length === 0) s.push([u, u + c.length]);
				else {
					const d = s[s.length - 1];
					d[1] < u ? s.push([u, u + c.length]) : d[1] = u + c.length;
				}
				a = u + c.length;
			}
			return {
				matches: s,
				score: Yw(s, t.length, r.length, o)
			};
		}
		function Gw(e$1) {
			for (var t = e$1.toLowerCase(), n = [], i = 0, r = 0; r < t.length; r++) {
				const o = t.charAt(r);
				jw.test(o) ? (i !== r && n.push(t.substring(i, r)), i = r + 1) : (_w.test(o) || Ww.test(o)) && (i !== r && n.push(t.substring(i, r)), n.push(o), i = r + 1);
			}
			return i !== t.length && n.push(t.substring(i, t.length)), {
				query: e$1,
				tokens: n,
				fuzzy: t.split("")
			};
		}
		function Xw(e$1, t) {
			if (e$1.query === "") return {
				score: 0,
				matches: []
			};
			const n = $w(e$1.tokens, e$1.query, t, !1);
			return n || $w(e$1.fuzzy, e$1.query, t, !0);
		}
		const e = Gw(inputText);
		let container = this.getCommandsContainer();
		if (container.classList.contains("suggestions")) this.exit();
		if (inputText.trim() == "") return true;
		const suggestions = [];
		this.getCommandsContainer().querySelectorAll(".command").forEach((commandNode) => {
			const spanNode = commandNode.querySelector(".name span");
			const spanText = spanNode.innerText;
			const res = Xw(e, spanText);
			if (res) {
				commandNode = this.createCommandNode(commandNode.command);
				let spanHTML = "";
				let i = 0;
				for (let j = 0; j < res.matches.length; j++) {
					const [start, end] = res.matches[j];
					if (start > i) spanHTML += spanText.slice(i, start);
					spanHTML += `<span class="highlight">${spanText.slice(start, end)}</span>`;
					i = end;
				}
				if (i < spanText.length) spanHTML += spanText.slice(i, spanText.length);
				commandNode.querySelector(".name span").innerHTML = spanHTML;
				suggestions.push({
					score: res.score,
					commandNode
				});
			}
		});
		if (suggestions.length > 0) {
			suggestions.sort((a, b) => b.score - a.score).slice(this.maxSuggestionNum);
			container = this.createCommandsContainer();
			container.classList.add("suggestions");
			suggestions.forEach((suggestion) => {
				container.appendChild(suggestion.commandNode);
			});
			return true;
		} else {
			const anonymousCommand = this.commands.find((c) => !c.name && (!c.when || c.when()));
			if (anonymousCommand) await this.execCallback(anonymousCommand.callback);
			else this.showTip(this.defaultText.empty);
			return false;
		}
	}
	/**
	* Bind events of pressing `keydown` and `keyup` key.
	*/
	initInputEvents() {
		this.promptNode.addEventListener("keydown", (event) => {
			if (["ArrowUp", "ArrowDown"].includes(event.key)) {
				event.preventDefault();
				let selectedIndex;
				const allItems = [...Array.from(this.getCommandsContainer().querySelectorAll(".command"))].filter((e) => e.style.display != "none");
				selectedIndex = allItems.findIndex((e) => e.classList.contains("selected"));
				if (selectedIndex != -1) {
					allItems[selectedIndex].classList.remove("selected");
					selectedIndex += event.key == "ArrowUp" ? -1 : 1;
				} else if (event.key == "ArrowUp") selectedIndex = allItems.length - 1;
				else selectedIndex = 0;
				if (selectedIndex == -1) selectedIndex = allItems.length - 1;
				else if (selectedIndex == allItems.length) selectedIndex = 0;
				allItems[selectedIndex].classList.add("selected");
				const commandsContainer = this.getCommandsContainer();
				commandsContainer.scrollTo(0, commandsContainer.querySelector(".selected").offsetTop - commandsContainer.offsetHeight + 7.5);
				allItems[selectedIndex].classList.add("selected");
			}
		});
		this.promptNode.addEventListener("keyup", async (event) => {
			if (event.key == "Enter") this.trigger();
			else if (event.key == "Escape") if (this.inputNode.value.length > 0) this.inputNode.value = "";
			else this.exit();
			else if (["ArrowUp", "ArrowDown"].includes(event.key)) return;
			const currentInputText = this.inputNode.value;
			if (currentInputText == this.lastInputText) return;
			this.lastInputText = currentInputText;
			window.setTimeout(async () => {
				await this.showSuggestions(currentInputText);
			});
		});
	}
	/**
	* Create a commandsContainer and display a text
	*/
	showTip(text) {
		const tipNode = this.ui.createElement(this.document, "div", {
			classList: ["tip"],
			properties: { innerText: text }
		});
		const container = this.createCommandsContainer();
		container.classList.add("suggestions");
		container.appendChild(tipNode);
		return tipNode;
	}
	/**
	* Mark the selected item with class `selected`.
	* @param item HTMLDivElement
	*/
	selectItem(item) {
		this.getCommandsContainer().querySelectorAll(".command").forEach((e) => e.classList.remove("selected"));
		item.classList.add("selected");
	}
	addStyle() {
		const style$1 = this.ui.createElement(this.document, "style", {
			namespace: "html",
			id: "prompt-style"
		});
		style$1.innerText = `
      .prompt-container * {
        box-sizing: border-box;
      }
      .prompt-container {
        ---radius---: 10px;
        position: fixed;
        left: 25%;
        top: 10%;
        width: 50%;
        border-radius: var(---radius---);
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        font-size: 18px;
        box-shadow: 0px 1.8px 7.3px rgba(0, 0, 0, 0.071),
                    0px 6.3px 24.7px rgba(0, 0, 0, 0.112),
                    0px 30px 90px rgba(0, 0, 0, 0.2);
        font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Microsoft YaHei Light", sans-serif;
        background-color: var(--material-background) !important;
        border: var(--material-border-quarternary) !important;
      }
      
      /* input */
      .prompt-container .input-container  {
        width: 100%;
      }

      .input-container input {
        width: -moz-available;
        height: 40px;
        padding: 24px;
        border: none;
        outline: none;
        font-size: 18px;
        margin: 0 !important;
        border-radius: var(---radius---);
        background-color: var(--material-background);
      }
      
      .input-container .cta {
        border-bottom: var(--material-border-quarternary);
        margin: 5px auto;
      }
      
      /* results */
      .commands-containers {
        width: 100%;
        height: 100%;
      }
      .commands-container {
        max-height: calc(${this.maxLineNum} * 35.5px);
        width: calc(100% - 12px);
        margin-left: 12px;
        margin-right: 0%;
        overflow-y: auto;
        overflow-x: hidden;
      }
      
      .commands-container .command {
        display: flex;
        align-content: baseline;
        justify-content: space-between;
        border-radius: 5px;
        padding: 6px 12px;
        margin-right: 12px;
        margin-top: 2px;
        margin-bottom: 2px;
      }
      .commands-container .command .content {
        display: flex;
        width: 100%;
        justify-content: space-between;
        flex-direction: row;
        overflow: hidden;
      }
      .commands-container .command .content .name {
        white-space: nowrap; 
        text-overflow: ellipsis;
        overflow: hidden;
      }
      .commands-container .command .content .aux {
        display: flex;
        align-items: center;
        align-self: center;
        flex-shrink: 0;
      }
      
      .commands-container .command .content .aux .label {
        font-size: 15px;
        color: var(--fill-primary);
        padding: 2px 6px;
        background-color: var(--color-background);
        border-radius: 5px;
      }
      
      .commands-container .selected {
          background-color: var(--material-mix-quinary);
      }

      .commands-container .highlight {
        font-weight: bold;
      }

      .tip {
        color: var(--fill-primary);
        text-align: center;
        padding: 12px 12px;
        font-size: 18px;
      }

      /* instructions */
      .instructions {
        display: flex;
        align-content: center;
        justify-content: center;
        font-size: 15px;
        height: 2.5em;
        width: 100%;
        border-top: var(--material-border-quarternary);
        color: var(--fill-secondary);
        margin-top: 5px;
      }
      
      .instructions .instruction {
        margin: auto .5em;  
      }
      
      .instructions .key {
        margin-right: .2em;
        font-weight: 600;
      }
    `;
		this.document.documentElement.appendChild(style$1);
	}
	registerShortcut() {
		this.document.addEventListener("keydown", (event) => {
			if (event.shiftKey && event.key.toLowerCase() == "p") {
				if (event.originalTarget.isContentEditable || "value" in event.originalTarget || this.commands.length == 0) return;
				event.preventDefault();
				event.stopPropagation();
				if (this.promptNode.style.display == "none") {
					this.promptNode.style.display = "flex";
					if (this.promptNode.querySelectorAll(".commands-container").length == 1) this.showCommands(this.commands, true);
					this.promptNode.focus();
					this.inputNode.focus();
				} else this.promptNode.style.display = "none";
			}
		}, true);
	}
};
var PromptManager = class extends ManagerTool {
	prompt;
	/**
	* Save the commands registered from this manager
	*/
	commands = [];
	constructor(base) {
		super(base);
		const globalCache = toolkitGlobal_default.getInstance()?.prompt;
		if (!globalCache) throw new Error("Prompt is not initialized.");
		if (!globalCache._ready) {
			globalCache._ready = true;
			globalCache.instance = new Prompt();
		}
		this.prompt = globalCache.instance;
	}
	/**
	* Register commands. Don't forget to call `unregister` on plugin exit.
	* @param commands Command[]
	* @example
	* ```ts
	* let getReader = () => {
	*   return BasicTool.getZotero().Reader.getByTabID(
	*     (Zotero.getMainWindow().Zotero_Tabs).selectedID
	*   )
	* }
	*
	* register([
	*   {
	*     name: "Split Horizontally",
	*     label: "Zotero",
	*     when: () => getReader() as boolean,
	*     callback: (prompt: Prompt) => getReader().menuCmd("splitHorizontally")
	*   },
	*   {
	*     name: "Split Vertically",
	*     label: "Zotero",
	*     when: () => getReader() as boolean,
	*     callback: (prompt: Prompt) => getReader().menuCmd("splitVertically")
	*   }
	* ])
	* ```
	*/
	register(commands) {
		commands.forEach((c) => c.id ??= c.name);
		this.prompt.commands = [...this.prompt.commands, ...commands];
		this.commands = [...this.commands, ...commands];
		this.prompt.showCommands(this.commands, true);
	}
	/**
	* You can delete a command registed before by its name.
	* @remarks
	* There is a premise here that the names of all commands registered by a single plugin are not duplicated.
	* @param id Command.name
	*/
	unregister(id) {
		this.prompt.commands = this.prompt.commands.filter((c) => c.id != id);
		this.commands = this.commands.filter((c) => c.id != id);
	}
	/**
	* Call `unregisterAll` on plugin exit.
	*/
	unregisterAll() {
		this.prompt.commands = this.prompt.commands.filter((c) => {
			return this.commands.every((_c) => _c.id != c.id);
		});
		this.commands = [];
	}
};

//#endregion
//#region src/tools/extraField.ts
/**
* Get/set extra field APIs.
*/
var ExtraFieldTool = class extends BasicTool {
	/**
	* Get all extra fields
	* @param item
	*/
	getExtraFields(item, backend = "custom") {
		const extraFiledRaw = item.getField("extra");
		if (backend === "default") return this.getGlobal("Zotero").Utilities.Internal.extractExtraFields(extraFiledRaw).fields;
		else {
			const map = /* @__PURE__ */ new Map();
			const nonStandardFields = [];
			extraFiledRaw.split("\n").forEach((line) => {
				const split = line.split(": ");
				if (split.length >= 2 && split[0]) map.set(split[0], split.slice(1).join(": "));
				else nonStandardFields.push(line);
			});
			map.set("__nonStandard__", nonStandardFields.join("\n"));
			return map;
		}
	}
	/**
	* Get extra field value by key. If it does not exists, return undefined.
	* @param item
	* @param key
	*/
	getExtraField(item, key) {
		const fields = this.getExtraFields(item);
		return fields.get(key);
	}
	/**
	* Replace extra field of an item.
	* @param item
	* @param fields
	*/
	async replaceExtraFields(item, fields) {
		const kvs = [];
		if (fields.has("__nonStandard__")) {
			kvs.push(fields.get("__nonStandard__"));
			fields.delete("__nonStandard__");
		}
		fields.forEach((v, k) => {
			kvs.push(`${k}: ${v}`);
		});
		item.setField("extra", kvs.join("\n"));
		await item.saveTx();
	}
	/**
	* Set an key-value pair to the item's extra field
	* @param item
	* @param key
	* @param value
	*/
	async setExtraField(item, key, value) {
		const fields = this.getExtraFields(item);
		if (value === "" || typeof value === "undefined") fields.delete(key);
		else fields.set(key, value);
		await this.replaceExtraFields(item, fields);
	}
};

//#endregion
//#region src/tools/reader.ts
/**
* Zotero ReaderInstance APIs.
*/
var ReaderTool = class extends BasicTool {
	/**
	* Get the selected tab reader.
	* @param waitTime Wait for n MS until the reader is ready
	*/
	async getReader(waitTime = 5e3) {
		const Zotero_Tabs = this.getGlobal("Zotero_Tabs");
		if (Zotero_Tabs.selectedType !== "reader") return void 0;
		let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
		let delayCount = 0;
		const checkPeriod = 50;
		while (!reader && delayCount * checkPeriod < waitTime) {
			await new Promise((resolve) => setTimeout(resolve, checkPeriod));
			reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
			delayCount++;
		}
		await reader?._initPromise;
		return reader;
	}
	/**
	* Get all window readers.
	*/
	getWindowReader() {
		const Zotero_Tabs = this.getGlobal("Zotero_Tabs");
		const windowReaders = [];
		const tabs = Zotero_Tabs._tabs.map((e) => e.id);
		for (let i = 0; i < Zotero.Reader._readers.length; i++) {
			let flag = false;
			for (let j = 0; j < tabs.length; j++) if (Zotero.Reader._readers[i].tabID === tabs[j]) {
				flag = true;
				break;
			}
			if (!flag) windowReaders.push(Zotero.Reader._readers[i]);
		}
		return windowReaders;
	}
	/**
	* Get Reader tabpanel deck element.
	* @deprecated - use item pane api
	* @alpha
	*/
	getReaderTabPanelDeck() {
		const deck = this.getGlobal("window").document.querySelector(".notes-pane-deck")?.previousElementSibling;
		return deck;
	}
	/**
	* Add a reader tabpanel deck selection change observer.
	* @deprecated - use item pane api
	* @alpha
	* @param callback
	*/
	async addReaderTabPanelDeckObserver(callback) {
		await waitUtilAsync(() => !!this.getReaderTabPanelDeck());
		const deck = this.getReaderTabPanelDeck();
		const observer = new (this.getGlobal("MutationObserver"))(async (mutations) => {
			mutations.forEach(async (mutation) => {
				const target = mutation.target;
				if (target.classList.contains("zotero-view-tabbox") || target.tagName === "deck") callback();
			});
		});
		observer.observe(deck, {
			attributes: true,
			attributeFilter: ["selectedIndex"],
			subtree: true
		});
		return observer;
	}
	/**
	* Get the selected annotation data.
	* @param reader Target reader
	* @returns The selected annotation data.
	*/
	getSelectedAnnotationData(reader) {
		const annotation = reader?._internalReader._lastView._selectionPopup?.annotation;
		return annotation;
	}
	/**
	* Get the text selection of reader.
	* @param reader Target reader
	* @returns The text selection of reader.
	*/
	getSelectedText(reader) {
		return this.getSelectedAnnotationData(reader)?.text ?? "";
	}
};

//#endregion
//#region src/ztoolkit.ts
/**
* ⭐Contains all tools in this lib. Start from here if you are new to this lib.
* @remarks
* To minimize your plugin, import the modules you need manually.
*/
var ZoteroToolkit = class extends BasicTool {
	static _version = BasicTool._version;
	UI = new UITool(this);
	Reader = new ReaderTool(this);
	ExtraField = new ExtraFieldTool(this);
	FieldHooks = new FieldHookManager(this);
	Keyboard = new KeyboardManager(this);
	Prompt = new PromptManager(this);
	Menu = new MenuManager(this);
	Clipboard = makeHelperTool(ClipboardHelper, this);
	FilePicker = makeHelperTool(FilePickerHelper, this);
	Patch = makeHelperTool(PatchHelper, this);
	ProgressWindow = makeHelperTool(ProgressWindowHelper, this);
	VirtualizedTable = makeHelperTool(VirtualizedTableHelper, this);
	Dialog = makeHelperTool(DialogHelper, this);
	LargePrefObject = makeHelperTool(LargePrefHelper, this);
	Guide = makeHelperTool(GuideHelper, this);
	constructor() {
		super();
	}
	/**
	* Unregister everything created by managers.
	*/
	unregisterAll() {
		unregister(this);
	}
};

//#endregion
export { BasicTool, ClipboardHelper, DialogHelper, ExtraFieldTool, FieldHookManager, FilePickerHelper, GuideHelper, KeyModifier, KeyboardManager, LargePrefHelper, ManagerTool, MenuManager, MessageHelper, PatchHelper, ProgressWindowHelper, Prompt, PromptManager, ReaderTool, SettingsDialogHelper, UITool, VirtualizedTableHelper, ZoteroToolkit, _importESModule, makeHelperTool, unregister, wait_exports as wait };