mirror of
https://github.com/logseq/logseq.git
synced 2026-05-16 17:02:34 +00:00
This pull request refactors the plugin library infrastructure and adds new experimental features for hosted/sidebar renderers. The main changes include: Purpose: Refactor plugin communication library (Postmate) to support MessageChannel for improved performance, add support for hosted/sidebar renderers in plugins, add new debug APIs, and consolidate helper functions. Changes: Added MessageChannel support to Postmate for optimized plugin-host communication with backward compatibility Introduced hosted renderer and sidebar renderer APIs for plugins to register custom UI components Added new app APIs: get_current_route, export_debug_log_db, reset_debug_log_db Refactored helper functions from helpers.ts to common.ts and updated all import paths Extended block property APIs to include class properties with default values Added comprehensive documentation for experiments APIs and plugin development Added E2E test for plugin marketplace installation Version bump from 0.2.12 to 0.3.1
919 lines
21 KiB
TypeScript
919 lines
21 KiB
TypeScript
import {
|
|
isValidUUID,
|
|
deepMerge,
|
|
mergeSettingsWithSchema,
|
|
PluginLogger,
|
|
safeSnakeCase,
|
|
safetyPathJoin, normalizeKeyStr,
|
|
} from './common'
|
|
import { LSPluginCaller } from './LSPlugin.caller'
|
|
import * as callableAPIs from './callable.apis'
|
|
import {
|
|
IAppProxy,
|
|
IDBProxy,
|
|
IEditorProxy,
|
|
ILSPluginUser,
|
|
LSPluginBaseInfo,
|
|
LSPluginUserEvents,
|
|
SlashCommandAction,
|
|
BlockCommandCallback,
|
|
StyleString,
|
|
Theme,
|
|
UIOptions,
|
|
IHookEvent,
|
|
BlockIdentity,
|
|
BlockPageName,
|
|
UIContainerAttrs,
|
|
SimpleCommandCallback,
|
|
SimpleCommandKeybinding,
|
|
SettingSchemaDesc,
|
|
IUserOffHook,
|
|
IGitProxy,
|
|
IUIProxy,
|
|
UserProxyNSTags,
|
|
BlockUUID,
|
|
BlockEntity,
|
|
IDatom,
|
|
IAssetsProxy,
|
|
AppInfo,
|
|
IPluginSearchServiceHooks,
|
|
PageEntity, IUtilsProxy,
|
|
} from './LSPlugin'
|
|
import Debug from 'debug'
|
|
import * as CSS from 'csstype'
|
|
import EventEmitter from 'eventemitter3'
|
|
import { IAsyncStorage, LSPluginFileStorage } from './modules/LSPlugin.Storage'
|
|
import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
|
|
import { LSPluginRequest } from './modules/LSPlugin.Request'
|
|
import { LSPluginSearchService } from './modules/LSPlugin.Search'
|
|
|
|
declare global {
|
|
interface Window {
|
|
__LSP__HOST__: boolean
|
|
logseq: LSPluginUser
|
|
}
|
|
}
|
|
|
|
type callableMethods = keyof typeof callableAPIs | string // host exported SDK apis & host platform related apis
|
|
|
|
const PROXY_CONTINUE = Symbol.for('proxy-continue')
|
|
const debug = Debug('LSPlugin:user')
|
|
const logger = new PluginLogger('', { console: true })
|
|
|
|
/**
|
|
* @param type (key of group commands)
|
|
* @param opts
|
|
* @param action
|
|
*/
|
|
function registerSimpleCommand(
|
|
this: LSPluginUser,
|
|
type: string,
|
|
opts: {
|
|
key: string
|
|
label: string
|
|
desc?: string
|
|
palette?: boolean
|
|
keybinding?: SimpleCommandKeybinding
|
|
extras?: Record<string, any>
|
|
},
|
|
action: SimpleCommandCallback
|
|
) {
|
|
const { key, label, desc, palette, keybinding, extras } = opts
|
|
|
|
if (typeof action !== 'function') {
|
|
this.logger.error(`${key || label}: command action should be function.`)
|
|
return false
|
|
}
|
|
|
|
const normalizedKey = normalizeKeyStr(key)
|
|
|
|
if (!normalizedKey) {
|
|
this.logger.error(`${label}: command key is required.`)
|
|
return false
|
|
}
|
|
|
|
const eventKey = `SimpleCommandHook${normalizedKey}${++registeredCmdUid}`
|
|
|
|
this.Editor['on' + eventKey](action)
|
|
|
|
this.caller?.call(`api:call`, {
|
|
method: 'register-plugin-simple-command',
|
|
args: [
|
|
this.baseInfo.id,
|
|
// [cmd, action]
|
|
[
|
|
{ key: normalizedKey, label, type, desc, keybinding, extras },
|
|
['editor/hook', eventKey],
|
|
],
|
|
palette,
|
|
],
|
|
})
|
|
}
|
|
|
|
function shouldValidUUID(uuid: string) {
|
|
if (!isValidUUID(uuid)) {
|
|
logger.error(`#${uuid} is not a valid UUID string.`)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
function checkEffect(p: LSPluginUser) {
|
|
return p && (p.baseInfo?.effect || !p.baseInfo?.iir)
|
|
}
|
|
|
|
let _appBaseInfo: AppInfo = null
|
|
let _searchServices: Map<string, LSPluginSearchService> = new Map()
|
|
|
|
const app: Partial<IAppProxy> = {
|
|
async getInfo(this: LSPluginUser, key) {
|
|
if (!_appBaseInfo) {
|
|
_appBaseInfo = await this._execCallableAPIAsync('get-app-info')
|
|
}
|
|
return typeof key === 'string' ? _appBaseInfo[key] : _appBaseInfo
|
|
},
|
|
|
|
registerCommand: registerSimpleCommand,
|
|
|
|
registerSearchService<T extends IPluginSearchServiceHooks>(
|
|
this: LSPluginUser,
|
|
s: T
|
|
) {
|
|
if (_searchServices.has(s.name)) {
|
|
throw new Error(`SearchService: #${s.name} has registered!`)
|
|
}
|
|
|
|
_searchServices.set(s.name, new LSPluginSearchService(this, s))
|
|
},
|
|
|
|
registerCommandPalette(
|
|
opts: { key: string; label: string; keybinding?: SimpleCommandKeybinding },
|
|
action: SimpleCommandCallback
|
|
) {
|
|
const { key, label, keybinding } = opts
|
|
const group = '$palette$'
|
|
|
|
return registerSimpleCommand.call(
|
|
this,
|
|
group,
|
|
{ key, label, palette: true, keybinding },
|
|
action
|
|
)
|
|
},
|
|
|
|
registerCommandShortcut(
|
|
keybinding: SimpleCommandKeybinding | string,
|
|
action: SimpleCommandCallback,
|
|
opts: Partial<{
|
|
key: string
|
|
label: string
|
|
desc: string
|
|
extras: Record<string, any>
|
|
}> = {}
|
|
) {
|
|
if (typeof keybinding == 'string') {
|
|
keybinding = {
|
|
mode: 'global',
|
|
binding: keybinding,
|
|
}
|
|
}
|
|
|
|
const { binding } = keybinding
|
|
const group = '$shortcut$'
|
|
const key = opts.key || (group + safeSnakeCase(binding?.toString()))
|
|
|
|
return registerSimpleCommand.call(
|
|
this,
|
|
group,
|
|
{ ...opts, key, palette: false, keybinding },
|
|
action
|
|
)
|
|
},
|
|
|
|
registerUIItem(
|
|
type: 'toolbar' | 'pagebar',
|
|
opts: { key: string; template: string }
|
|
) {
|
|
const pid = this.baseInfo.id
|
|
// opts.key = `${pid}_${opts.key}`
|
|
|
|
this.caller?.call(`api:call`, {
|
|
method: 'register-plugin-ui-item',
|
|
args: [pid, type, opts],
|
|
})
|
|
},
|
|
|
|
registerPageMenuItem(
|
|
this: LSPluginUser,
|
|
tag: string,
|
|
action: (e: IHookEvent & { page: string }) => void
|
|
) {
|
|
if (typeof action !== 'function') {
|
|
return false
|
|
}
|
|
|
|
const key = tag + '_' + this.baseInfo.id
|
|
const label = tag
|
|
const type = 'page-menu-item'
|
|
|
|
registerSimpleCommand.call(
|
|
this,
|
|
type,
|
|
{
|
|
key,
|
|
label,
|
|
},
|
|
action
|
|
)
|
|
},
|
|
|
|
onBlockRendererSlotted(uuid, callback: (payload: any) => void) {
|
|
if (!shouldValidUUID(uuid)) return
|
|
|
|
const pid = this.baseInfo.id
|
|
const hook = `hook:editor:${safeSnakeCase(`slot:${uuid}`)}`
|
|
|
|
this.caller.on(hook, callback)
|
|
this.App._installPluginHook(pid, hook)
|
|
|
|
return () => {
|
|
this.caller.off(hook, callback)
|
|
this.App._uninstallPluginHook(pid, hook)
|
|
}
|
|
},
|
|
|
|
invokeExternalPlugin(this: LSPluginUser, type: string, ...args: Array<any>) {
|
|
type = type?.trim()
|
|
if (!type) return
|
|
let [pid, group] = type.split('.')
|
|
if (!['models', 'commands'].includes(group?.toLowerCase())) {
|
|
throw new Error(`Type only support '.models' or '.commands' currently.`)
|
|
}
|
|
const key = type.replace(`${pid}.${group}.`, '')
|
|
|
|
if (!pid || !group || !key) {
|
|
throw new Error(`Illegal type of #${type} to invoke external plugin.`)
|
|
}
|
|
return this._execCallableAPIAsync(
|
|
'invoke_external_plugin_cmd',
|
|
pid,
|
|
group.toLowerCase(),
|
|
key,
|
|
args
|
|
)
|
|
},
|
|
|
|
setFullScreen(flag) {
|
|
const sf = (...args) => this._callWin('setFullScreen', ...args)
|
|
|
|
if (flag === 'toggle') {
|
|
this._callWin('isFullScreen').then((r) => {
|
|
r ? sf() : sf(true)
|
|
})
|
|
} else {
|
|
flag ? sf(true) : sf()
|
|
}
|
|
},
|
|
}
|
|
|
|
let registeredCmdUid = 0
|
|
|
|
const editor: Partial<IEditorProxy> = {
|
|
newBlockUUID(this: LSPluginUser): Promise<string> {
|
|
return this._execCallableAPIAsync('new_block_uuid')
|
|
},
|
|
|
|
isPageBlock(
|
|
this: LSPluginUser,
|
|
block: BlockEntity | PageEntity
|
|
): Boolean {
|
|
return block.uuid && block.hasOwnProperty('name')
|
|
},
|
|
|
|
registerSlashCommand(
|
|
this: LSPluginUser,
|
|
tag: string,
|
|
actions: BlockCommandCallback | Array<SlashCommandAction>
|
|
) {
|
|
debug('Register slash command #', this.baseInfo.id, tag, actions)
|
|
|
|
if (typeof actions === 'function') {
|
|
actions = [
|
|
['editor/clear-current-slash', false],
|
|
['editor/restore-saved-cursor'],
|
|
['editor/hook', actions],
|
|
]
|
|
}
|
|
|
|
actions = actions.map((it) => {
|
|
const [tag, ...args] = it
|
|
|
|
switch (tag) {
|
|
case 'editor/hook':
|
|
let key = args[0]
|
|
let fn = () => {
|
|
this.caller?.callUserModel(key)
|
|
}
|
|
|
|
if (typeof key === 'function') {
|
|
fn = key
|
|
}
|
|
|
|
const eventKey = `SlashCommandHook${tag}${++registeredCmdUid}`
|
|
|
|
it[1] = eventKey
|
|
|
|
// register command listener
|
|
this.Editor['on' + eventKey](fn)
|
|
break
|
|
default:
|
|
}
|
|
|
|
return it
|
|
})
|
|
|
|
this.caller?.call(`api:call`, {
|
|
method: 'register-plugin-slash-command',
|
|
args: [this.baseInfo.id, [tag, actions]],
|
|
})
|
|
},
|
|
|
|
registerBlockContextMenuItem(
|
|
this: LSPluginUser,
|
|
label: string,
|
|
action: BlockCommandCallback
|
|
) {
|
|
if (typeof action !== 'function') {
|
|
return false
|
|
}
|
|
|
|
const key = label + '_' + this.baseInfo.id
|
|
const type = 'block-context-menu-item'
|
|
|
|
registerSimpleCommand.call(
|
|
this,
|
|
type,
|
|
{
|
|
key,
|
|
label,
|
|
},
|
|
action
|
|
)
|
|
},
|
|
|
|
registerHighlightContextMenuItem(
|
|
this: LSPluginUser,
|
|
label: string,
|
|
action: SimpleCommandCallback,
|
|
opts?: { clearSelection: boolean }
|
|
) {
|
|
if (typeof action !== 'function') {
|
|
return false
|
|
}
|
|
|
|
const key = label + '_' + this.baseInfo.id
|
|
const type = 'highlight-context-menu-item'
|
|
|
|
registerSimpleCommand.call(
|
|
this,
|
|
type,
|
|
{
|
|
key,
|
|
label,
|
|
extras: opts,
|
|
},
|
|
action
|
|
)
|
|
},
|
|
|
|
scrollToBlockInPage(
|
|
this: LSPluginUser,
|
|
pageName: BlockPageName,
|
|
blockId: BlockIdentity,
|
|
opts?: { replaceState: boolean }
|
|
) {
|
|
const anchor = `block-content-` + blockId
|
|
if (opts?.replaceState) {
|
|
this.App.replaceState('page', { name: pageName }, { anchor })
|
|
} else {
|
|
this.App.pushState('page', { name: pageName }, { anchor })
|
|
}
|
|
},
|
|
}
|
|
|
|
const db: Partial<IDBProxy> = {
|
|
onBlockChanged(
|
|
this: LSPluginUser,
|
|
uuid: BlockUUID,
|
|
callback: (
|
|
block: BlockEntity,
|
|
txData: Array<IDatom>,
|
|
txMeta?: { outlinerOp: string; [p: string]: any }
|
|
) => void
|
|
): IUserOffHook {
|
|
if (!shouldValidUUID(uuid)) return
|
|
|
|
const pid = this.baseInfo.id
|
|
const hook = `hook:db:${safeSnakeCase(`block:${uuid}`)}`
|
|
const aBlockChange = ({ block, txData, txMeta }) => {
|
|
if (block.uuid !== uuid) {
|
|
return
|
|
}
|
|
|
|
callback(block, txData, txMeta)
|
|
}
|
|
|
|
this.caller.on(hook, aBlockChange)
|
|
this.App._installPluginHook(pid, hook)
|
|
|
|
return () => {
|
|
this.caller.off(hook, aBlockChange)
|
|
this.App._uninstallPluginHook(pid, hook)
|
|
}
|
|
},
|
|
|
|
datascriptQuery<T = any>(
|
|
this: LSPluginUser,
|
|
query: string,
|
|
...inputs: Array<any>
|
|
): Promise<T> {
|
|
// force remove proxy ns flag `db`
|
|
inputs.pop()
|
|
|
|
if (inputs?.some((it) => typeof it === 'function')) {
|
|
const host = this.Experiments.ensureHostScope()
|
|
return host.logseq.api.datascript_query(query, ...inputs)
|
|
}
|
|
|
|
return this._execCallableAPIAsync(`datascript_query`, ...[query, ...inputs])
|
|
},
|
|
}
|
|
|
|
const git: Partial<IGitProxy> = {}
|
|
|
|
const ui: Partial<IUIProxy> = {}
|
|
|
|
const utils: Partial<IUtilsProxy> = {}
|
|
|
|
const assets: Partial<IAssetsProxy> = {
|
|
makeSandboxStorage(this: LSPluginUser): IAsyncStorage {
|
|
return new LSPluginFileStorage(this, { assets: true })
|
|
},
|
|
}
|
|
|
|
type uiState = {
|
|
key?: number
|
|
visible: boolean
|
|
}
|
|
|
|
const KEY_MAIN_UI = 0
|
|
|
|
/**
|
|
* User plugin instance from global namespace `logseq`.
|
|
* @example
|
|
* ```ts
|
|
* logseq.UI.showMsg('Hello, Logseq')
|
|
* ```
|
|
* @public
|
|
*/
|
|
export class LSPluginUser
|
|
extends EventEmitter<LSPluginUserEvents>
|
|
implements ILSPluginUser {
|
|
// @ts-ignore
|
|
private _version: string = LIB_VERSION
|
|
private _debugTag: string = ''
|
|
private _settingsSchema?: Array<SettingSchemaDesc>
|
|
private _connected: boolean = false
|
|
|
|
/**
|
|
* ui frame identities
|
|
* @private
|
|
*/
|
|
private _ui = new Map<number, uiState>()
|
|
|
|
private _mFileStorage: LSPluginFileStorage
|
|
private _mRequest: LSPluginRequest
|
|
private _mExperiments: LSPluginExperiments
|
|
|
|
/**
|
|
* handler of before unload plugin
|
|
* @private
|
|
*/
|
|
private _beforeunloadCallback?: (e: any) => Promise<void>
|
|
|
|
/**
|
|
* @param _baseInfo
|
|
* @param _caller
|
|
*/
|
|
constructor(
|
|
private _baseInfo: LSPluginBaseInfo,
|
|
private _caller: LSPluginCaller
|
|
) {
|
|
super()
|
|
|
|
_caller.on('sys:ui:visible', (payload) => {
|
|
if (payload?.toggle) {
|
|
this.toggleMainUI()
|
|
}
|
|
})
|
|
|
|
_caller.on('settings:changed', (payload) => {
|
|
const prevSettings = { ...(this.settings || {}) }
|
|
const nextSettings = { ...(payload || {}) }
|
|
|
|
this._baseInfo = {
|
|
...this._baseInfo,
|
|
settings: nextSettings,
|
|
}
|
|
|
|
this.emit('settings:changed', nextSettings, prevSettings)
|
|
})
|
|
|
|
_caller.on('beforeunload', async (payload) => {
|
|
const { actor, ...rest } = payload
|
|
const cb = this._beforeunloadCallback
|
|
|
|
try {
|
|
cb && (await cb(rest))
|
|
actor?.resolve(null)
|
|
} catch (e) {
|
|
this.logger.error(`[beforeunload] `, e)
|
|
actor?.reject(e)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Life related
|
|
async ready(model?: any, callback?: any) {
|
|
if (this._connected) return
|
|
|
|
try {
|
|
if (typeof model === 'function') {
|
|
callback = model
|
|
model = {}
|
|
}
|
|
|
|
let baseInfo = await this._caller.connectToParent(model)
|
|
const hostSettings = baseInfo?.settings
|
|
|
|
this._connected = true
|
|
|
|
baseInfo = deepMerge(this._baseInfo, baseInfo)
|
|
if (hostSettings !== undefined) {
|
|
baseInfo.settings = hostSettings
|
|
}
|
|
this._baseInfo = baseInfo
|
|
|
|
if (baseInfo?.id) {
|
|
this._debugTag =
|
|
this._caller.debugTag = `#${baseInfo.id} [${baseInfo.name}]`
|
|
|
|
this.logger.setTag(this._debugTag)
|
|
}
|
|
|
|
if (this._settingsSchema) {
|
|
baseInfo.settings = mergeSettingsWithSchema(
|
|
baseInfo.settings,
|
|
this._settingsSchema
|
|
)
|
|
|
|
// TODO: sync host settings schema
|
|
await this.useSettingsSchema(this._settingsSchema)
|
|
}
|
|
|
|
try {
|
|
await this._execCallableAPIAsync('setSDKMetadata', {
|
|
version: this._version,
|
|
runtime: 'js',
|
|
})
|
|
} catch (e) {
|
|
console.warn(e)
|
|
}
|
|
|
|
callback && callback.call(this, baseInfo)
|
|
} catch (e) {
|
|
console.error(`${this._debugTag} [Ready Error]`, e)
|
|
}
|
|
}
|
|
|
|
ensureConnected() {
|
|
if (!this._connected) {
|
|
throw new Error('not connected')
|
|
}
|
|
}
|
|
|
|
beforeunload(callback: (e: any) => Promise<void>): void {
|
|
if (typeof callback !== 'function') return
|
|
this._beforeunloadCallback = callback
|
|
}
|
|
|
|
provideModel(model: Record<string, any>) {
|
|
this.caller._extendUserModel(model)
|
|
return this
|
|
}
|
|
|
|
provideTheme(theme: Theme) {
|
|
this.caller.call('provider:theme', theme)
|
|
return this
|
|
}
|
|
|
|
provideStyle(style: StyleString) {
|
|
this.caller.call('provider:style', style)
|
|
return this
|
|
}
|
|
|
|
provideUI(ui: UIOptions) {
|
|
this.caller.call('provider:ui', ui)
|
|
return this
|
|
}
|
|
|
|
// Settings related
|
|
useSettingsSchema(schema: Array<SettingSchemaDesc>) {
|
|
if (this.connected) {
|
|
this.caller.call('settings:schema', {
|
|
schema,
|
|
isSync: true,
|
|
})
|
|
}
|
|
|
|
this._settingsSchema = schema
|
|
return this
|
|
}
|
|
|
|
updateSettings(attrs: Record<string, any>) {
|
|
this.caller.call('settings:update', attrs)
|
|
// TODO: update associated baseInfo settings
|
|
}
|
|
|
|
onSettingsChanged<T = any>(cb: (a: T, b: T) => void): IUserOffHook {
|
|
const type = 'settings:changed'
|
|
this.on(type, cb)
|
|
return () => this.off(type, cb)
|
|
}
|
|
|
|
showSettingsUI() {
|
|
this.caller.call('settings:visible:changed', { visible: true })
|
|
}
|
|
|
|
hideSettingsUI() {
|
|
this.caller.call('settings:visible:changed', { visible: false })
|
|
}
|
|
|
|
// UI related
|
|
setMainUIAttrs(attrs: Partial<UIContainerAttrs>): void {
|
|
this.caller.call('main-ui:attrs', attrs)
|
|
}
|
|
|
|
setMainUIInlineStyle(style: CSS.Properties): void {
|
|
this.caller.call('main-ui:style', style)
|
|
}
|
|
|
|
hideMainUI(opts?: { restoreEditingCursor: boolean }): void {
|
|
const payload = {
|
|
key: KEY_MAIN_UI,
|
|
visible: false,
|
|
cursor: opts?.restoreEditingCursor,
|
|
}
|
|
this.caller.call('main-ui:visible', payload)
|
|
this.emit('ui:visible:changed', payload)
|
|
this._ui.set(payload.key, payload)
|
|
}
|
|
|
|
showMainUI(opts?: { autoFocus: boolean }): void {
|
|
const payload = {
|
|
key: KEY_MAIN_UI,
|
|
visible: true,
|
|
autoFocus: opts?.autoFocus,
|
|
}
|
|
this.caller.call('main-ui:visible', payload)
|
|
this.emit('ui:visible:changed', payload)
|
|
this._ui.set(payload.key, payload)
|
|
}
|
|
|
|
toggleMainUI(): void {
|
|
const payload = { key: KEY_MAIN_UI, toggle: true }
|
|
const state = this._ui.get(payload.key)
|
|
if (state && state.visible) {
|
|
this.hideMainUI()
|
|
} else {
|
|
this.showMainUI()
|
|
}
|
|
}
|
|
|
|
// Getters
|
|
get version(): string {
|
|
return this._version
|
|
}
|
|
|
|
get isMainUIVisible(): boolean {
|
|
const state = this._ui.get(KEY_MAIN_UI)
|
|
return Boolean(state && state.visible)
|
|
}
|
|
|
|
get connected(): boolean {
|
|
return this._connected
|
|
}
|
|
|
|
get baseInfo(): LSPluginBaseInfo {
|
|
return this._baseInfo
|
|
}
|
|
|
|
get effect(): Boolean {
|
|
return checkEffect(this)
|
|
}
|
|
|
|
get logger() {
|
|
return logger
|
|
}
|
|
|
|
get settings() {
|
|
return this.baseInfo?.settings
|
|
}
|
|
|
|
get caller(): LSPluginCaller {
|
|
return this._caller
|
|
}
|
|
|
|
resolveResourceFullUrl(filePath: string) {
|
|
this.ensureConnected()
|
|
if (!filePath) return
|
|
filePath = filePath.replace(/^[.\\/]+/, '')
|
|
return safetyPathJoin(this._baseInfo.lsr, filePath)
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
_makeUserProxy(target: any, nstag?: UserProxyNSTags) {
|
|
const that = this
|
|
const caller = this.caller
|
|
|
|
return new Proxy(target, {
|
|
get(target: any, propKey, _receiver) {
|
|
const origMethod = target[propKey]
|
|
|
|
return function (this: any, ...args: any) {
|
|
if (origMethod) {
|
|
if (args?.length !== 0) args.push(nstag)
|
|
const ret = origMethod.apply(that, args)
|
|
if (ret !== PROXY_CONTINUE) return ret
|
|
}
|
|
|
|
// Handle hook
|
|
if (nstag) {
|
|
const hookMatcher = propKey.toString().match(/^(once|off|on)/i)
|
|
|
|
if (hookMatcher != null) {
|
|
const f = hookMatcher[0].toLowerCase()
|
|
const s = hookMatcher.input!
|
|
const isOff = f === 'off'
|
|
const pid = that.baseInfo.id
|
|
|
|
let type = s.slice(f.length)
|
|
let handler = args[0]
|
|
let opts = args[1]
|
|
|
|
// condition mode
|
|
if (typeof handler === 'string' && typeof opts === 'function') {
|
|
handler = handler.replace(/^logseq./, ':')
|
|
type = `${type}${handler}`
|
|
handler = opts
|
|
opts = args[2]
|
|
}
|
|
|
|
type = `hook:${nstag}:${safeSnakeCase(type)}`
|
|
|
|
caller[f](type, handler)
|
|
|
|
const unlisten = () => {
|
|
caller.off(type, handler)
|
|
if (!caller.listenerCount(type)) {
|
|
that.App._uninstallPluginHook(pid, type)
|
|
}
|
|
}
|
|
|
|
if (!isOff) {
|
|
that.App._installPluginHook(pid, type, opts)
|
|
} else {
|
|
unlisten()
|
|
return
|
|
}
|
|
|
|
return unlisten
|
|
}
|
|
}
|
|
|
|
let method = propKey as string
|
|
|
|
// TODO: refactor api call with the explicit tag
|
|
if ((['git', 'ui', 'assets', 'utils'] as UserProxyNSTags[]).includes(nstag)) {
|
|
method = nstag + '_' + method
|
|
}
|
|
|
|
// Call host
|
|
return caller.callAsync(`api:call`, {
|
|
tag: nstag,
|
|
method,
|
|
args: args,
|
|
})
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
_execCallableAPIAsync(method: callableMethods, ...args) {
|
|
return this._caller.callAsync(`api:call`, {
|
|
method,
|
|
args,
|
|
})
|
|
}
|
|
|
|
_execCallableAPI(method: callableMethods, ...args) {
|
|
this._caller.call(`api:call`, {
|
|
method,
|
|
args,
|
|
})
|
|
}
|
|
|
|
_callWin(...args) {
|
|
return this._execCallableAPIAsync(`_callMainWin`, ...args)
|
|
}
|
|
|
|
// User Proxies
|
|
#appProxy: IAppProxy
|
|
#editorProxy: IEditorProxy
|
|
#dbProxy: IDBProxy
|
|
#uiProxy: IUIProxy
|
|
#utilsProxy: IUtilsProxy
|
|
|
|
get App(): IAppProxy {
|
|
if (this.#appProxy) return this.#appProxy
|
|
return (this.#appProxy = this._makeUserProxy(app, 'app'))
|
|
}
|
|
|
|
get Editor(): IEditorProxy {
|
|
if (this.#editorProxy) return this.#editorProxy
|
|
return (this.#editorProxy = this._makeUserProxy(editor, 'editor'))
|
|
}
|
|
|
|
get DB(): IDBProxy {
|
|
if (this.#dbProxy) return this.#dbProxy
|
|
return (this.#dbProxy = this._makeUserProxy(db, 'db'))
|
|
}
|
|
|
|
get UI(): IUIProxy {
|
|
if (this.#uiProxy) return this.#uiProxy
|
|
return (this.#uiProxy = this._makeUserProxy(ui, 'ui'))
|
|
}
|
|
|
|
get Utils(): IUtilsProxy {
|
|
if (this.#utilsProxy) return this.#utilsProxy
|
|
return (this.#utilsProxy = this._makeUserProxy(utils, 'utils'))
|
|
}
|
|
|
|
get Git(): IGitProxy {
|
|
return this._makeUserProxy(git, 'git')
|
|
}
|
|
|
|
get Assets(): IAssetsProxy {
|
|
return this._makeUserProxy(assets, 'assets')
|
|
}
|
|
|
|
get FileStorage(): LSPluginFileStorage {
|
|
let m = this._mFileStorage
|
|
if (!m) m = this._mFileStorage = new LSPluginFileStorage(this)
|
|
return m
|
|
}
|
|
|
|
get Request(): LSPluginRequest {
|
|
let m = this._mRequest
|
|
if (!m) m = this._mRequest = new LSPluginRequest(this)
|
|
return m
|
|
}
|
|
|
|
get Experiments(): LSPluginExperiments {
|
|
let m = this._mExperiments
|
|
if (!m) m = this._mExperiments = new LSPluginExperiments(this)
|
|
return m
|
|
}
|
|
}
|
|
|
|
export * from './LSPlugin'
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
export function setupPluginUserInstance(
|
|
pluginBaseInfo: LSPluginBaseInfo,
|
|
pluginCaller: LSPluginCaller
|
|
) {
|
|
return new LSPluginUser(pluginBaseInfo, pluginCaller)
|
|
}
|
|
|
|
// entry of iframe mode
|
|
if (window.__LSP__HOST__ == null) {
|
|
const caller = new LSPluginCaller(null)
|
|
window.logseq = setupPluginUserInstance({} as any, caller)
|
|
}
|