Enhance/more ns plugin api (#4828)

* improve(plugin): WIP add settings schema

* improve(plugin): add identity for settings modal

* improve(plugin): WIP add settings input

* fix(ui): scrollbar overlay of modal panel content

* improve(plugin): WIP add more render types of setting item

* improve(plugin): WIP polish settings items

* improve(plugin): WIP settings list of plugins

* improve(plugin): more settings types & polish releated ui

* fix(plugin): sometimes disable plugin not work

* improve(plugin): polish ui of plugin settings

* fix(dev): warning of lint

* improve(plugin): add api of settings changed

* chore: build libs core

* fix(ui): width of settings panel wrap

* improve(plugin): separate layouts data from settings aio file

* imporve(plugin): container size of single plugin settings

* fix: add missing state

* improve(plugin): add Git ns

* improve(plugin): git related api

* improve(api): type of git result

* chore: build libs core

* fix(dev): kondo lint

* fix(plugin): use cdn sdk when js entry

* chore: build libs core

* fix(plugin): env condition

* improve(plugin): add UI ns

* fix(api): arguments of datascript query

* enhance(api): manageable message instance of UI tools

* enhance(api): WIP add experiments api

* enhance(api): WIP add resources state of plugin

* improve(plugin): add status of loading script resources

* improve(plugin): more opts for script loader

* improve(plugin): WIP add fenced code renderer hook

* improve(plugin): fenced code renderer hook

* fix(plugin): resource root path of plugin fs location

* imporve(plugin): support local files for loading scripts

* improve(plugin): types of expirements api

* fix: typo of class

* enhance(api): add namespace related apis

* enhance(api): add linked refrences related apis

* enhance(plugin): add sample links to related api comments

* improve(plugin): add db changed hook & optimize strategy of caller for hooks

* improve(plugin): compatible commands registration for old sdk

* improve(plugin): collect user sdk version for plugin local

* improve(plugin): add internal callable apis for user sdk

* chore(plugin): missing files & bump libs version

* improve(plugin): compatiable for old sdk about hook messaging optimization

* improve(plugin): db hook optimization for old sdk

* enhance(ux): auto focus searchbar when open plugins list

* improve(plugin): api of a hook from specific block changed event

* improve(plugin): api of db block change hook

* improve(plugin): add show bracket user config of api

* improve(plugin): api of db block change hook

* fix(api): toggle collapsed of block

* improve(api): try to init grpah with git before exec git commands

* improve(plugin): attributes of sandbox container

* improve(dev): support register command with keybinding

* improve(plugin): add api of register shortcut command

* fix(plugin): reubild slash commands when new command registration

* fix(dev): lint

* improve(dev): lint script of libs codebase

* chore(dev): remove useless codes

* improve(plugin):sanitize path string of plugin repo value

* fix(plugin): rebuild commands list when unregister a plugin

* fix(ui): overflow width of query result table

* chore: rebuild libs core

* improve(plugin): add assets related apis

* chore: rebuild libs core

* improve(plugin): support replace state of into block in page api

* improve(plugin): prepend/append child block in page

* improve(plugin): polished exceptions message of plugin update/install

* fix(plugin): update settings within gui

* improve(ux): debounce change event of input for plugin settings gui

* chore: rebuild libs core

* enhance(plugin): catch exception of hook plugin
This commit is contained in:
Charlie
2022-04-21 18:43:16 +08:00
committed by GitHub
parent a87e5ea0fa
commit 79bc33e1e3
35 changed files with 3444 additions and 937 deletions

4
libs/.prettierrc.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
...require('prettier-config-standard'),
trailingComma: 'es5'
}

View File

@@ -1,6 +1,6 @@
{
"name": "@logseq/libs",
"version": "0.0.1-alpha.35",
"version": "0.0.2",
"description": "Logseq SDK libraries",
"main": "dist/lsplugin.user.js",
"typings": "index.d.ts",
@@ -10,7 +10,9 @@
"dev:user": "npm run build:user -- --mode development --watch",
"build:core": "webpack --config webpack.config.core.js --mode production",
"dev:core": "npm run build:core -- --mode development --watch",
"build": "tsc && rm dist/*.js && npm run build:user"
"build": "tsc && rm dist/*.js && npm run build:user",
"lint": "prettier --check \"src/**/*.{ts, js}\"",
"fix": "prettier --write \"src/**/*.{ts, js}\""
},
"dependencies": {
"csstype": "3.0.8",
@@ -26,6 +28,8 @@
"@types/debug": "^4.1.5",
"@types/dompurify": "^2.2.1",
"@types/lodash-es": "^4.17.4",
"prettier": "^2.6.2",
"prettier-config-standard": "^5.0.0",
"ts-loader": "^8.0.17",
"typescript": "^4.2.2",
"webpack": "^5.24.3",

View File

@@ -33,14 +33,16 @@ class LSPluginCaller extends EventEmitter {
private _status?: 'pending' | 'timeout'
private _userModel: any = {}
private _call?: (type: string, payload: any, actor?: DeferredActor) => Promise<any>
private _call?: (
type: string,
payload: any,
actor?: DeferredActor
) => Promise<any>
private _callUserModel?: (type: string, payload: any) => Promise<any>
private _debugTag = ''
constructor (
private _pluginLocal: PluginLocal | null
) {
constructor(private _pluginLocal: PluginLocal | null) {
super()
if (_pluginLocal) {
@@ -75,8 +77,14 @@ class LSPluginCaller extends EventEmitter {
const model: any = this._extendUserModel({
[LSPMSG_READY]: async (baseInfo) => {
// dynamically setup common msg handler
model[LSPMSGFn(baseInfo?.pid)] = ({ type, payload }: { type: string, payload: any }) => {
debug(`[call from host (_call)] ${this._debugTag}`, type, payload)
model[LSPMSGFn(baseInfo?.pid)] = ({
type,
payload,
}: {
type: string
payload: any
}) => {
debug(`[host (_call) -> *user] ${this._debugTag}`, type, payload)
// host._call without async
caller.emit(type, payload)
}
@@ -95,7 +103,10 @@ class LSPluginCaller extends EventEmitter {
},
[LSPMSG]: async ({ ns, type, payload }: any) => {
debug(`[call from host (async)] ${this._debugTag}`, ns, type, payload)
debug(
`[host (async) -> *user] ${this._debugTag} ns=${ns} type=${type}`,
payload
)
if (ns && ns.startsWith('hook')) {
caller.emit(`${ns}:${type}`, payload)
@@ -106,7 +117,7 @@ class LSPluginCaller extends EventEmitter {
},
[LSPMSG_SYNC]: ({ _sync, result }: any) => {
debug(`[sync reply] #${_sync}`, result)
debug(`[sync host -> *user] #${_sync}`, result)
if (syncActors.has(_sync)) {
const actor = syncActors.get(_sync)
@@ -123,7 +134,7 @@ class LSPluginCaller extends EventEmitter {
}
},
...userModel
...userModel,
})
if (isShadowMode) {
@@ -136,7 +147,8 @@ class LSPluginCaller extends EventEmitter {
this._status = 'pending'
await handshake.then((refParent: ChildAPI) => {
await handshake
.then((refParent: ChildAPI) => {
this._child = refParent
this._connected = true
@@ -173,7 +185,8 @@ class LSPluginCaller extends EventEmitter {
}
}
}, 1000 * 60 * 30)
}).finally(() => {
})
.finally(() => {
this._status = undefined
})
@@ -186,6 +199,7 @@ class LSPluginCaller extends EventEmitter {
return this._call?.call(this, type, payload)
}
// only for callable apis for sdk user
async callAsync(type: any, payload: any = {}) {
const actor = deferred(1000 * 10)
return this._call?.call(this, type, payload, actor)
@@ -199,18 +213,22 @@ class LSPluginCaller extends EventEmitter {
async _setupIframeSandbox() {
const pl = this._pluginLocal!
const id = pl.id
const domId = `${id}_lsp_main`
const url = new URL(pl.options.entry!)
url.searchParams
.set(`__v__`, IS_DEV ? Date.now().toString() : pl.options.version)
url.searchParams.set(
`__v__`,
IS_DEV ? Date.now().toString() : pl.options.version
)
// clear zombie sandbox
const zb = document.querySelector(`#${id}`)
const zb = document.querySelector(`#${domId}`)
if (zb) zb.parentElement.removeChild(zb)
const cnt = document.createElement('div')
cnt.classList.add('lsp-iframe-sandbox-container')
cnt.id = id
cnt.id = domId
cnt.dataset.pid = id
// TODO: apply any container layout data
try {
@@ -219,20 +237,24 @@ class LSPluginCaller extends EventEmitter {
cnt.dataset.inited_layout = 'true'
const { width, height, left, top } = mainLayoutInfo
Object.assign(cnt.style, {
width: width + 'px', height: height + 'px',
left: left + 'px', top: top + 'px'
width: width + 'px',
height: height + 'px',
left: left + 'px',
top: top + 'px',
})
}
} catch (e) {
console.error("[Restore Layout Error]", e)
console.error('[Restore Layout Error]', e)
}
document.body.appendChild(cnt)
const pt = new Postmate({
id: id + '_iframe', container: cnt, url: url.href,
id: id + '_iframe',
container: cnt,
url: url.href,
classListArray: ['lsp-iframe-sandbox'],
model: { baseInfo: JSON.parse(JSON.stringify(pl.toJSON())) }
model: { baseInfo: JSON.parse(JSON.stringify(pl.toJSON())) },
})
let handshake = pt.sendHandshake()
@@ -246,13 +268,14 @@ class LSPluginCaller extends EventEmitter {
reject(new Error(`handshake Timeout`))
}, 3 * 1000) // 3secs
handshake.then((refChild: ParentAPI) => {
handshake
.then((refChild: ParentAPI) => {
this._parent = refChild
this._connected = true
this.emit('connected')
refChild.on(LSPMSGFn(pl.id), ({ type, payload }: any) => {
debug(`[call from plugin] `, type, payload)
debug(`[user -> *host] `, type, payload)
this._pluginLocal?.emit(type, payload || {})
})
@@ -260,9 +283,10 @@ class LSPluginCaller extends EventEmitter {
this._call = async (...args: any) => {
// parent all will get message before handshaked
await refChild.call(LSPMSGFn(pl.id), {
type: args[0], payload: Object.assign(args[1] || {}, {
$$pid: pl.id
})
type: args[0],
payload: Object.assign(args[1] || {}, {
$$pid: pl.id,
}),
})
}
@@ -276,22 +300,26 @@ class LSPluginCaller extends EventEmitter {
}
resolve(null)
}).catch(e => {
})
.catch((e) => {
reject(e)
}).finally(() => {
})
.finally(() => {
clearTimeout(timer)
})
}).catch(e => {
})
.catch((e) => {
debug('[iframe sandbox] error', e)
throw e
}).finally(() => {
})
.finally(() => {
this._status = undefined
})
}
async _setupShadowSandbox() {
const pl = this._pluginLocal!
const shadow = this._shadow = new LSPluginShadowFrame(pl)
const shadow = (this._shadow = new LSPluginShadowFrame(pl))
try {
this._status = 'pending'
@@ -305,9 +333,12 @@ class LSPluginCaller extends EventEmitter {
actor && (payload.actor = actor)
// @ts-ignore Call in same thread
this._pluginLocal?.emit(type, Object.assign(payload, {
$$pid: pl.id
}))
this._pluginLocal?.emit(
type,
Object.assign(payload, {
$$pid: pl.id,
})
)
return actor?.promise
}
@@ -374,6 +405,4 @@ class LSPluginCaller extends EventEmitter {
}
}
export {
LSPluginCaller
}
export { LSPluginCaller }

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@ function userFetch (url, opts) {
resolve({
text() {
return content
}
},
})
} catch (e) {
console.error(e)
@@ -32,9 +32,7 @@ class LSPluginShadowFrame extends EventEmitter<'mounted' | 'unmounted'> {
private _loaded = false
private _unmountFns: Array<() => Promise<void>> = []
constructor (
private _pluginLocal: PluginLocal
) {
constructor(private _pluginLocal: PluginLocal) {
super()
_pluginLocal._dispose(() => {
@@ -47,15 +45,15 @@ class LSPluginShadowFrame extends EventEmitter<'mounted' | 'unmounted'> {
if (this.loaded || !entry) return
const { template, execScripts } = await importHTML(entry, { fetch: userFetch })
const { template, execScripts } = await importHTML(entry, {
fetch: userFetch,
})
this._mount(template, document.body)
const sandbox = createSandboxContainer(
name, {
const sandbox = createSandboxContainer(name, {
elementGetter: () => this._root?.firstChild,
}
)
})
const global = sandbox.instance.proxy as any
@@ -76,7 +74,7 @@ class LSPluginShadowFrame extends EventEmitter<'mounted' | 'unmounted'> {
}
_mount(content: string, container: HTMLElement) {
const frame = this._frame = document.createElement('div')
const frame = (this._frame = document.createElement('div'))
frame.classList.add('lsp-shadow-sandbox')
frame.id = this._pluginLocal.id
@@ -111,6 +109,4 @@ class LSPluginShadowFrame extends EventEmitter<'mounted' | 'unmounted'> {
}
}
export {
LSPluginShadowFrame
}
export { LSPluginShadowFrame }

View File

@@ -2,6 +2,7 @@ import EventEmitter from 'eventemitter3'
import * as CSS from 'csstype'
import { LSPluginCaller } from './LSPlugin.caller'
import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
export type PluginLocalIdentity = string
@@ -14,7 +15,7 @@ export type ThemeOptions = {
[key: string]: any
}
export type StyleString = string;
export type StyleString = string
export type StyleOptions = {
key?: string
style: StyleString
@@ -76,7 +77,7 @@ export interface LSPluginBaseInfo {
settings: {
disabled: boolean
[key: string]: any
},
}
[key: string]: any
}
@@ -86,15 +87,26 @@ export type IHookEvent = {
}
export type IUserOffHook = () => void
export type IUserHook<E = any, R = IUserOffHook> = (callback: (e: IHookEvent & E) => void) => IUserOffHook
export type IUserSlotHook<E = any> = (callback: (e: IHookEvent & UISlotIdentity & E) => void) => void
export type IUserHook<E = any, R = IUserOffHook> = (
callback: (e: IHookEvent & E) => void
) => IUserOffHook
export type IUserSlotHook<E = any> = (
callback: (e: IHookEvent & UISlotIdentity & E) => void
) => void
export type EntityID = number
export type BlockUUID = string
export type BlockUUIDTuple = ['uuid', BlockUUID]
export type IEntityID = { id: EntityID }
export type IBatchBlock = { content: string, properties?: Record<string, any>, children?: Array<IBatchBlock> }
export type IEntityID = { id: EntityID; [key: string]: any }
export type IBatchBlock = {
content: string
properties?: Record<string, any>
children?: Array<IBatchBlock>
}
export type IDatom = [e: number, a: string, v: any, t: number, added: boolean]
export type IGitResult = { stdout: string; stderr: string; exitCode: number }
export interface AppUserInfo {
[key: string]: any
@@ -111,6 +123,9 @@ export interface AppUserConfigs {
preferredLanguage: string
preferredWorkflow: string
currentGraph: string
showBracket: boolean
[key: string]: any
}
@@ -145,7 +160,7 @@ export interface BlockEntity {
container?: string
file?: IEntityID
level?: number
meta?: { timestamps: any, properties: any, startPos: number, endPos: number }
meta?: { timestamps: any; properties: any; startPos: number; endPos: number }
title?: Array<any>
[key: string]: any
@@ -163,6 +178,7 @@ export interface PageEntity {
file?: IEntityID
namespace?: IEntityID
children?: Array<PageEntity>
format?: 'markdown' | 'org'
journalDay?: number
}
@@ -171,18 +187,26 @@ export type BlockIdentity = BlockUUID | Pick<BlockEntity, 'uuid'>
export type BlockPageName = string
export type PageIdentity = BlockPageName | BlockIdentity
export type SlashCommandActionCmd =
'editor/input'
| 'editor/input'
| 'editor/hook'
| 'editor/clear-current-slash'
| 'editor/restore-saved-cursor'
export type SlashCommandAction = [cmd: SlashCommandActionCmd, ...args: any]
export type SimpleCommandCallback = (e: IHookEvent) => void
export type BlockCommandCallback = (e: IHookEvent & { uuid: BlockUUID }) => Promise<void>
export type BlockCursorPosition = { left: number, top: number, height: number, pos: number, rect: DOMRect }
export type BlockCommandCallback = (
e: IHookEvent & { uuid: BlockUUID }
) => Promise<void>
export type BlockCursorPosition = {
left: number
top: number
height: number
pos: number
rect: DOMRect
}
export type SimpleCommandKeybinding = {
mode?: 'global' | 'non-editing' | 'editing',
binding: string,
mode?: 'global' | 'non-editing' | 'editing'
binding: string
mac?: string // special for Mac OS
}
@@ -198,48 +222,50 @@ export type SettingSchemaDesc = {
}
export type ExternalCommandType =
'logseq.command/run' |
'logseq.editor/cycle-todo' |
'logseq.editor/down' |
'logseq.editor/up' |
'logseq.editor/expand-block-children' |
'logseq.editor/collapse-block-children' |
'logseq.editor/open-file-in-default-app' |
'logseq.editor/open-file-in-directory' |
'logseq.editor/select-all-blocks' |
'logseq.editor/toggle-open-blocks' |
'logseq.editor/zoom-in' |
'logseq.editor/zoom-out' |
'logseq.editor/indent' |
'logseq.editor/outdent' |
'logseq.editor/copy' |
'logseq.editor/cut' |
'logseq.go/home' |
'logseq.go/journals' |
'logseq.go/keyboard-shortcuts' |
'logseq.go/next-journal' |
'logseq.go/prev-journal' |
'logseq.go/search' |
'logseq.go/search-in-page' |
'logseq.go/tomorrow' |
'logseq.go/backward' |
'logseq.go/forward' |
'logseq.search/re-index' |
'logseq.sidebar/clear' |
'logseq.sidebar/open-today-page' |
'logseq.ui/goto-plugins' |
'logseq.ui/select-theme-color' |
'logseq.ui/toggle-brackets' |
'logseq.ui/toggle-cards' |
'logseq.ui/toggle-contents' |
'logseq.ui/toggle-document-mode' |
'logseq.ui/toggle-help' |
'logseq.ui/toggle-left-sidebar' |
'logseq.ui/toggle-right-sidebar' |
'logseq.ui/toggle-settings' |
'logseq.ui/toggle-theme' |
'logseq.ui/toggle-wide-mode' |
'logseq.command-palette/toggle'
| 'logseq.command/run'
| 'logseq.editor/cycle-todo'
| 'logseq.editor/down'
| 'logseq.editor/up'
| 'logseq.editor/expand-block-children'
| 'logseq.editor/collapse-block-children'
| 'logseq.editor/open-file-in-default-app'
| 'logseq.editor/open-file-in-directory'
| 'logseq.editor/select-all-blocks'
| 'logseq.editor/toggle-open-blocks'
| 'logseq.editor/zoom-in'
| 'logseq.editor/zoom-out'
| 'logseq.editor/indent'
| 'logseq.editor/outdent'
| 'logseq.editor/copy'
| 'logseq.editor/cut'
| 'logseq.go/home'
| 'logseq.go/journals'
| 'logseq.go/keyboard-shortcuts'
| 'logseq.go/next-journal'
| 'logseq.go/prev-journal'
| 'logseq.go/search'
| 'logseq.go/search-in-page'
| 'logseq.go/tomorrow'
| 'logseq.go/backward'
| 'logseq.go/forward'
| 'logseq.search/re-index'
| 'logseq.sidebar/clear'
| 'logseq.sidebar/open-today-page'
| 'logseq.ui/goto-plugins'
| 'logseq.ui/select-theme-color'
| 'logseq.ui/toggle-brackets'
| 'logseq.ui/toggle-cards'
| 'logseq.ui/toggle-contents'
| 'logseq.ui/toggle-document-mode'
| 'logseq.ui/toggle-help'
| 'logseq.ui/toggle-left-sidebar'
| 'logseq.ui/toggle-right-sidebar'
| 'logseq.ui/toggle-settings'
| 'logseq.ui/toggle-theme'
| 'logseq.ui/toggle-wide-mode'
| 'logseq.command-palette/toggle'
export type UserProxyTags = 'app' | 'editor' | 'db' | 'git' | 'ui' | 'assets'
/**
* App level APIs
@@ -253,25 +279,33 @@ export interface IAppProxy {
registerCommand: (
type: string,
opts: {
key: string,
label: string,
desc?: string,
palette?: boolean,
key: string
label: string
desc?: string
palette?: boolean
keybinding?: SimpleCommandKeybinding
},
action: SimpleCommandCallback) => void
action: SimpleCommandCallback
) => void
registerCommandPalette: (
opts: {
key: string,
label: string,
key: string
label: string
keybinding?: SimpleCommandKeybinding
},
action: SimpleCommandCallback) => void
action: SimpleCommandCallback
) => void
registerCommandShortcut: (
keybinding: SimpleCommandKeybinding,
action: SimpleCommandCallback
) => void
invokeExternalCommand: (
type: ExternalCommandType,
...args: Array<any>) => Promise<void>
...args: Array<any>
) => Promise<void>
/**
* Get state from app store
@@ -284,8 +318,7 @@ export interface IAppProxy {
* ```
* @param path
*/
getStateFromStore:
<T = any>(path: string | Array<string>) => Promise<T>
getStateFromStore: <T = any>(path: string | Array<string>) => Promise<T>
// native
relaunch: () => Promise<void>
@@ -293,6 +326,7 @@ export interface IAppProxy {
openExternalLink: (url: string) => Promise<void>
/**
* @deprecated Using `logseq.Git.execCommand`
* @link https://github.com/desktop/dugite/blob/master/docs/api/exec.md
* @param args
*/
@@ -302,12 +336,23 @@ export interface IAppProxy {
getCurrentGraph: () => Promise<AppGraphInfo | null>
// router
pushState: (k: string, params?: Record<string, any>, query?: Record<string, any>) => void
replaceState: (k: string, params?: Record<string, any>, query?: Record<string, any>) => void
pushState: (
k: string,
params?: Record<string, any>,
query?: Record<string, any>
) => void
replaceState: (
k: string,
params?: Record<string, any>,
query?: Record<string, any>
) => void
// ui
queryElementById: (id: string) => Promise<string | boolean>
showMsg: (content: string, status?: 'success' | 'warning' | 'error' | string) => void
showMsg: (
content: string,
status?: 'success' | 'warning' | 'error' | string
) => void
setZoomFactor: (factor: number) => void
setFullScreen: (flag: boolean | 'toggle') => void
setLeftSidebarVisible: (flag: boolean | 'toggle') => void
@@ -315,7 +360,7 @@ export interface IAppProxy {
registerUIItem: (
type: 'toolbar' | 'pagebar',
opts: { key: string, template: string }
opts: { key: string; template: string }
) => void
registerPageMenuItem: (
@@ -331,6 +376,7 @@ export interface IAppProxy {
/**
* provide ui slot to block `renderer` macro for `{{renderer arg1, arg2}}`
*
* @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-pomodoro-timer
* @example
* ```ts
* // e.g. {{renderer :h1, hello world, green}}
@@ -347,11 +393,17 @@ export interface IAppProxy {
* })
* ```
*/
onMacroRendererSlotted: IUserSlotHook<{ payload: { arguments: Array<string>, uuid: string, [key: string]: any } }>
onMacroRendererSlotted: IUserSlotHook<{
payload: { arguments: Array<string>; uuid: string; [key: string]: any }
}>
onPageHeadActionsSlotted: IUserSlotHook
onRouteChanged: IUserHook<{ path: string, template: string }>
onRouteChanged: IUserHook<{ path: string; template: string }>
onSidebarVisibleChanged: IUserHook<{ visible: boolean }>
// internal
_installPluginHook: (pid: string, hook: string) => void
_uninstallPluginHook: (pid: string, hookOrAll: string | boolean) => void
}
/**
@@ -360,10 +412,12 @@ export interface IAppProxy {
export interface IEditorProxy extends Record<string, any> {
/**
* register a custom command which will be added to the Logseq slash command list
*
* @param tag - displayed name of command
* @param action - can be a single callback function to run when the command is called, or an array of fixed commands with arguments
*
*
* @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-slash-commands
*
* @example
* ```ts
* logseq.Editor.registerSlashCommand("Say Hi", () => {
@@ -398,9 +452,6 @@ export interface IEditorProxy extends Record<string, any> {
checkEditing: () => Promise<BlockUUID | boolean>
/**
* insert a string at the current cursor
*/
insertAtEditingCursor: (content: string) => Promise<void>
restoreEditingCursor: () => Promise<void>
@@ -435,16 +486,52 @@ export interface IEditorProxy extends Record<string, any> {
*/
getPageBlocksTree: (srcPage: PageIdentity) => Promise<Array<BlockEntity>>
/**
* get all page/block linked references
* @param srcPage
*/
getPageLinkedReferences: (
srcPage: PageIdentity
) => Promise<Array<[page: PageEntity, blocks: Array<BlockEntity>]> | null>
/**
* get flatten pages from top namespace
* @param namespace
*/
getPagesFromNamespace: (
namespace: BlockPageName
) => Promise<Array<PageEntity> | null>
/**
* construct pages tree from namespace pages
* @param namespace
*/
getPagesTreeFromNamespace: (
namespace: BlockPageName
) => Promise<Array<PageEntity> | null>
/**
* @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-reddit-hot-news
*
* @param srcBlock
* @param content
* @param opts
*/
insertBlock: (
srcBlock: BlockIdentity,
content: string,
opts?: Partial<{ before: boolean; sibling: boolean; isPageBlock: boolean; properties: {} }>
opts?: Partial<{
before: boolean
sibling: boolean
isPageBlock: boolean
properties: {}
}>
) => Promise<BlockEntity | null>
insertBatchBlock: (
srcBlock: BlockIdentity,
batch: IBatchBlock | Array<IBatchBlock>,
opts?: Partial<{ before: boolean, sibling: boolean }>
opts?: Partial<{ before: boolean; sibling: boolean }>
) => Promise<Array<BlockEntity> | null>
updateBlock: (
@@ -453,9 +540,7 @@ export interface IEditorProxy extends Record<string, any> {
opts?: Partial<{ properties: {} }>
) => Promise<void>
removeBlock: (
srcBlock: BlockIdentity
) => Promise<void>
removeBlock: (srcBlock: BlockIdentity) => Promise<void>
getBlock: (
srcBlock: BlockIdentity | EntityID,
@@ -475,17 +560,24 @@ export interface IEditorProxy extends Record<string, any> {
createPage: (
pageName: BlockPageName,
properties?: {},
opts?: Partial<{ redirect: boolean, createFirstBlock: boolean, format: BlockEntity['format'], journal: boolean }>
opts?: Partial<{
redirect: boolean
createFirstBlock: boolean
format: BlockEntity['format']
journal: boolean
}>
) => Promise<PageEntity | null>
deletePage: (
pageName: BlockPageName
) => Promise<void>
deletePage: (pageName: BlockPageName) => Promise<void>
renamePage: (oldName: string, newName: string) => Promise<void>
getAllPages: (repo?: string) => Promise<any>
prependBlockInPage: (page: PageIdentity, content: string, opts?: Partial<{ properties: {} }>) => Promise<BlockEntity | null>
appendBlockInPage: (page: PageIdentity, content: string, opts?: Partial<{ properties: {} }>) => Promise<BlockEntity | null>
getPreviousSiblingBlock: (
srcBlock: BlockIdentity
) => Promise<BlockEntity | null>
@@ -514,13 +606,22 @@ export interface IEditorProxy extends Record<string, any> {
scrollToBlockInPage: (
pageName: BlockPageName,
blockId: BlockIdentity
blockId: BlockIdentity,
opts?: { replaceState: boolean }
) => void
openInRightSidebar: (uuid: BlockUUID) => void
// events
onInputSelectionEnd: IUserHook<{ caret: any, point: { x: number, y: number }, start: number, end: number, text: string }>
/**
* @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-a-translator
*/
onInputSelectionEnd: IUserHook<{
caret: any
point: { x: number; y: number }
start: number
end: number
text: string
}>
}
/**
@@ -537,7 +638,78 @@ export interface IDBProxy {
/**
* Run a datascript query
*/
datascriptQuery: <T = any>(query: string) => Promise<T>
datascriptQuery: <T = any>(query: string, ...inputs: Array<any>) => Promise<T>
/**
* Hook all transaction data of DB
*/
onChanged: IUserHook<{
blocks: Array<BlockEntity>
txData: Array<IDatom>
txMeta?: { outlinerOp: string; [key: string]: any }
}>
/**
* Subscribe a specific block changed event
*/
onBlockChanged(
uuid: BlockUUID,
callback: (
block: BlockEntity,
txData: Array<IDatom>,
txMeta?: { outlinerOp: string; [key: string]: any }
) => void
): IUserOffHook
}
/**
* Git related APIS
*/
export interface IGitProxy {
/**
* @link https://github.com/desktop/dugite/blob/master/docs/api/exec.md
* @param args
*/
execCommand: (args: string[]) => Promise<IGitResult>
loadIgnoreFile: () => Promise<string>
saveIgnoreFile: (content: string) => Promise<void>
}
/**
* UI related APIs
*/
export type UIMsgOptions = {
key: string
timeout: number // milliseconds. `0` indicate that keep showing
}
export type UIMsgKey = UIMsgOptions['key']
export interface IUIProxy {
showMsg: (
content: string,
status?: 'success' | 'warning' | 'error' | string,
opts?: Partial<UIMsgOptions>
) => Promise<UIMsgKey>
closeMsg: (key: UIMsgKey) => void
}
/**
* Assets related APIs
*/
export interface IAssetsProxy {
listFilesOfCurrentGraph(
exts: string | string[]
): Promise<{
path: string
size: number
accessTime: number
modifiedTime: number
changeTime: number
birthTime: number
}>
}
export interface ILSPluginThemeManager extends EventEmitter {
@@ -614,17 +786,13 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
/**
* Inject custom css for the main Logseq app
*
* @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-awesome-fonts
* @example
* ```ts
* logseq.provideStyle(`
* @import url("https://at.alicdn.com/t/font_2409735_r7em724douf.css");
* )
* ```
*
* @example
* ```ts
*
* ```
*/
provideStyle(style: StyleString | StyleOptions): this
@@ -632,6 +800,7 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
* Inject custom UI at specific DOM node.
* Event handlers can not be passed by string, so you need to create them in `provideModel`
*
* @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-a-translator
* @example
* ```ts
* logseq.provideUI({
@@ -647,8 +816,18 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
*/
provideUI(ui: UIOptions): this
/**
* @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-awesome-fonts
*
* @param schemas
*/
useSettingsSchema(schemas: Array<SettingSchemaDesc>): this
/**
* @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-awesome-fonts
*
* @param attrs
*/
updateSettings(attrs: Record<string, any>): void
onSettingsChanged<T = any>(cb: (a: T, b: T) => void): IUserOffHook
@@ -662,6 +841,7 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
/**
* Set the style for the plugin's UI
*
* @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-awesome-fonts
* @example
* ```ts
* logseq.setMainUIInlineStyle({
@@ -694,6 +874,9 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
App: IAppProxy & Record<string, any>
Editor: IEditorProxy & Record<string, any>
DB: IDBProxy
Git: IGitProxy
UI: IUIProxy
FileStorage: LSPluginFileStorage
Experiments: LSPluginExperiments
}

View File

@@ -1,7 +1,13 @@
import { deepMerge, mergeSettingsWithSchema, safetyPathJoin } from './helpers'
import {
deepMerge,
mergeSettingsWithSchema,
safeSnakeCase,
safetyPathJoin,
} from './helpers'
import { LSPluginCaller } from './LSPlugin.caller'
import {
IAppProxy, IDBProxy,
IAppProxy,
IDBProxy,
IEditorProxy,
ILSPluginUser,
LSPluginBaseInfo,
@@ -10,19 +16,33 @@ import {
BlockCommandCallback,
StyleString,
ThemeOptions,
UIOptions, IHookEvent, BlockIdentity,
UIOptions,
IHookEvent,
BlockIdentity,
BlockPageName,
UIContainerAttrs, SimpleCommandCallback, SimpleCommandKeybinding, SettingSchemaDesc, IUserOffHook
UIContainerAttrs,
SimpleCommandCallback,
SimpleCommandKeybinding,
SettingSchemaDesc,
IUserOffHook,
IGitProxy,
IUIProxy,
UserProxyTags,
BlockUUID,
BlockEntity,
IDatom,
IAssetsProxy,
} from './LSPlugin'
import Debug from 'debug'
import * as CSS from 'csstype'
import { snakeCase } from 'snake-case'
import EventEmitter from 'eventemitter3'
import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
declare global {
interface Window {
__LSP__HOST__: boolean
logseq: LSPluginUser
}
}
@@ -38,10 +58,10 @@ function registerSimpleCommand (
this: LSPluginUser,
type: string,
opts: {
key: string,
label: string,
desc?: string,
palette?: boolean,
key: string
label: string
desc?: string
palette?: boolean
keybinding?: SimpleCommandKeybinding
},
action: SimpleCommandCallback
@@ -57,7 +77,11 @@ function registerSimpleCommand (
this.caller?.call(`api:call`, {
method: 'register-plugin-simple-command',
args: [this.baseInfo.id, [{ key, label, type, desc, keybinding }, ['editor/hook', eventKey]], palette]
args: [
this.baseInfo.id,
[{ key, label, type, desc, keybinding }, ['editor/hook', eventKey]],
palette,
],
})
}
@@ -65,28 +89,46 @@ const app: Partial<IAppProxy> = {
registerCommand: registerSimpleCommand,
registerCommandPalette(
opts: { key: string; label: string, keybinding?: SimpleCommandKeybinding },
action: SimpleCommandCallback) {
opts: { key: string; label: string; keybinding?: SimpleCommandKeybinding },
action: SimpleCommandCallback
) {
const { key, label, keybinding } = opts
const group = 'global-palette-command'
const group = '$palette$'
return registerSimpleCommand.call(
this, group,
this,
group,
{ key, label, palette: true, keybinding },
action)
action
)
},
registerCommandShortcut(
keybinding: SimpleCommandKeybinding,
action: SimpleCommandCallback
) {
const { binding } = keybinding
const group = '$shortcut$'
const key = group + safeSnakeCase(binding)
return registerSimpleCommand.call(
this,
group,
{ key, palette: false, keybinding },
action
)
},
registerUIItem(
type: 'toolbar' | 'pagebar',
opts: { key: string, template: string }
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]
args: [pid, type, opts],
})
},
@@ -103,23 +145,28 @@ const app: Partial<IAppProxy> = {
const label = tag
const type = 'page-menu-item'
registerSimpleCommand.call(this,
type, {
key, label
}, action)
registerSimpleCommand.call(
this,
type,
{
key,
label,
},
action
)
},
setFullScreen(flag) {
const sf = (...args) => this._callWin('setFullScreen', ...args)
if (flag === 'toggle') {
this._callWin('isFullScreen').then(r => {
this._callWin('isFullScreen').then((r) => {
r ? sf() : sf(true)
})
} else {
flag ? sf(true) : sf()
}
}
},
}
let registeredCmdUid = 0
@@ -136,7 +183,7 @@ const editor: Partial<IEditorProxy> = {
actions = [
['editor/clear-current-slash', false],
['editor/restore-saved-cursor'],
['editor/hook', actions]
['editor/hook', actions],
]
}
@@ -169,7 +216,7 @@ const editor: Partial<IEditorProxy> = {
this.caller?.call(`api:call`, {
method: 'register-plugin-slash-command',
args: [this.baseInfo.id, [tag, actions]]
args: [this.baseInfo.id, [tag, actions]],
})
},
@@ -186,30 +233,68 @@ const editor: Partial<IEditorProxy> = {
const label = tag
const type = 'block-context-menu-item'
registerSimpleCommand.call(this,
type, {
key, label
}, action)
registerSimpleCommand.call(
this,
type,
{
key,
label,
},
action
)
},
scrollToBlockInPage(
this: LSPluginUser,
pageName: BlockPageName,
blockId: BlockIdentity
blockId: BlockIdentity,
opts?: { replaceState: boolean }
) {
const anchor = `block-content-` + blockId
this.App.pushState(
'page',
{ name: pageName },
{ anchor }
)
if (opts?.replaceState) {
this.App.replaceState('page', { name: pageName }, { anchor })
} else {
this.App.pushState('page', { name: pageName }, { anchor })
}
},
}
const db: Partial<IDBProxy> = {}
const db: Partial<IDBProxy> = {
onBlockChanged(
this: LSPluginUser,
uuid: BlockUUID,
callback: (
block: BlockEntity,
txData: Array<IDatom>,
txMeta?: { outlinerOp: string; [p: string]: any }
) => void
): IUserOffHook {
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)
}
},
}
const git: Partial<IGitProxy> = {}
const ui: Partial<IUIProxy> = {}
const assets: Partial<IAssetsProxy> = {}
type uiState = {
key?: number,
key?: number
visible: boolean
}
@@ -219,7 +304,12 @@ const KEY_MAIN_UI = 0
* User plugin instance
* @public
*/
export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements ILSPluginUser {
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
@@ -229,7 +319,8 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
*/
private _ui = new Map<number, uiState>()
private readonly _fileStorage: LSPluginFileStorage
private _mFileStorage: LSPluginFileStorage
private _mExperiments: LSPluginExperiments
/**
* handler of before unload plugin
@@ -264,22 +355,16 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
const cb = this._beforeunloadCallback
try {
cb && await cb(rest)
cb && (await cb(rest))
actor?.resolve(null)
} catch (e) {
console.debug(`${_caller.debugTag} [beforeunload] `, e)
actor?.reject(e)
}
})
// modules
this._fileStorage = new LSPluginFileStorage(this)
}
async ready (
model?: any,
callback?: any
) {
async ready(model?: any, callback?: any) {
if (this._connected) return
try {
@@ -296,7 +381,8 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
if (this._settingsSchema) {
baseInfo.settings = mergeSettingsWithSchema(
baseInfo.settings, this._settingsSchema
baseInfo.settings,
this._settingsSchema
)
// TODO: sync host settings schema
@@ -304,12 +390,17 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
}
if (baseInfo?.id) {
this._debugTag =
this._caller.debugTag = `#${baseInfo.id} [${baseInfo.name}]`
}
await this._execCallableAPIAsync('setSDKMetadata', {
version: this._version,
})
callback && callback.call(this, baseInfo)
} catch (e) {
console.error('[LSPlugin Ready Error]', e)
console.error(`${this._debugTag} [Ready Error]`, e)
}
}
@@ -347,7 +438,8 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
useSettingsSchema(schema: Array<SettingSchemaDesc>) {
if (this.connected) {
this.caller.call('settings:schema', {
schema, isSync: true
schema,
isSync: true,
})
}
@@ -383,14 +475,22 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
}
hideMainUI(opts?: { restoreEditingCursor: boolean }): void {
const payload = { key: KEY_MAIN_UI, visible: false, cursor: opts?.restoreEditingCursor }
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 }
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)
@@ -406,6 +506,10 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
}
}
get version(): string {
return this._version
}
get isMainUIVisible(): boolean {
const state = this._ui.get(KEY_MAIN_UI)
return Boolean(state && state.visible)
@@ -437,10 +541,7 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
/**
* @internal
*/
_makeUserProxy (
target: any,
tag?: 'app' | 'editor' | 'db'
) {
_makeUserProxy(target: any, tag?: UserProxyTags) {
const that = this
const caller = this.caller
@@ -450,7 +551,7 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
return function (this: any, ...args: any) {
if (origMethod) {
const ret = origMethod.apply(that, args)
const ret = origMethod.apply(that, args.concat(tag))
if (ret !== PROXY_CONTINUE) return
}
@@ -462,33 +563,59 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
const f = hookMatcher[0].toLowerCase()
const s = hookMatcher.input!
const e = s.slice(f.length)
const isOff = f === 'off'
const pid = that.baseInfo.id
const type = `hook:${tag}:${snakeCase(e)}`
const type = `hook:${tag}:${safeSnakeCase(e)}`
const handler = args[0]
caller[f](type, handler)
return f !== 'off' ? () => (caller.off(type, handler)) : void 0
if (isOff) {
return () => {
caller.off(type, handler)
that.App._uninstallPluginHook(pid, type)
}
} else {
return that.App._installPluginHook(pid, type)
}
}
}
let method = propKey as string
if ((['git', 'ui', 'assets'] as UserProxyTags[]).includes(tag)) {
method = tag + '_' + method
}
// Call host
return caller.callAsync(`api:call`, {
tag, method: propKey, args: args
tag,
method,
args: args,
})
}
}
},
})
}
/**
* @param args
*/
_callWin (...args) {
_execCallableAPIAsync(method, ...args) {
return this._caller.callAsync(`api:call`, {
method: '_callMainWin',
args: args
method,
args,
})
}
_execCallableAPI(method, ...args) {
this._caller.call(`api:call`, {
method,
args,
})
}
_callWin(...args) {
return this._execCallableAPIAsync(`_callMainWin`, ...args)
}
/**
* The interface methods of {@link IAppProxy}
*/
@@ -501,11 +628,31 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
}
get DB(): IDBProxy {
return this._makeUserProxy(db)
return this._makeUserProxy(db, 'db')
}
get Git(): IGitProxy {
return this._makeUserProxy(git, 'git')
}
get UI(): IUIProxy {
return this._makeUserProxy(ui, 'ui')
}
get Assets(): IAssetsProxy {
return this._makeUserProxy(assets, 'assets')
}
get FileStorage(): LSPluginFileStorage {
return this._fileStorage
let m = this._mFileStorage
if (!m) m = this._mFileStorage = new LSPluginFileStorage(this)
return m
}
get Experiments(): LSPluginExperiments {
let m = this._mExperiments
if (!m) m = this._mExperiments = new LSPluginExperiments(this)
return m
}
}
@@ -521,8 +668,8 @@ export function setupPluginUserInstance (
return new LSPluginUser(pluginBaseInfo, pluginCaller)
}
if (window.__LSP__HOST__ == null) { // Entry of iframe mode
// entry of iframe mode
if (window.__LSP__HOST__ == null) {
const caller = new LSPluginCaller(null)
// @ts-ignore
window.logseq = setupPluginUserInstance({} as any, caller)
}

View File

@@ -0,0 +1,7 @@
import { PluginLocal } from './LSPlugin.core'
export function setSDKMetadata(this: PluginLocal, data: any) {
if (this?.sdk && data) {
this.sdk = Object.assign({}, this.sdk, data)
}
}

View File

@@ -1,13 +1,10 @@
import { SettingSchemaDesc, StyleString, UIOptions } from './LSPlugin'
import { PluginLocal } from './LSPlugin.core'
import { snakeCase } from 'snake-case'
import * as nodePath from 'path'
import DOMPurify from 'dompurify'
import { merge } from 'lodash-es'
interface IObject {
[key: string]: any;
}
import { snakeCase } from 'snake-case'
import * as callables from './callable.apis'
declare global {
interface Window {
@@ -16,7 +13,8 @@ declare global {
}
}
export const path = navigator.platform.toLowerCase() === 'win32' ? nodePath.win32 : nodePath.posix
export const path =
navigator.platform.toLowerCase() === 'win32' ? nodePath.win32 : nodePath.posix
export const IS_DEV = process.env.NODE_ENV === 'development'
export const PROTOCOL_FILE = 'file://'
export const PROTOCOL_LSP = 'lsp://'
@@ -24,14 +22,18 @@ export const URL_LSP = PROTOCOL_LSP + 'logseq.io/'
let _appPathRoot
// TODO: snakeCase of lodash is incompatible with `snake-case`
export const safeSnakeCase = snakeCase
export async function getAppPathRoot(): Promise<string> {
if (_appPathRoot) {
return _appPathRoot
}
return (_appPathRoot =
await invokeHostExportedApi('_callApplication', 'getAppPath')
)
return (_appPathRoot = await invokeHostExportedApi(
'_callApplication',
'getAppPath'
))
}
export async function getSDKPathRoot(): Promise<string> {
@@ -46,7 +48,7 @@ export async function getSDKPathRoot (): Promise<string> {
}
export function isObject(item: any) {
return (item === Object(item) && !Array.isArray(item))
return item === Object(item) && !Array.isArray(item)
}
export const deepMerge = merge
@@ -112,38 +114,45 @@ export function deferred<T = any> (timeout?: number, tag?: string) {
if (timeout) {
// @ts-ignore
timeout = setTimeout(() => reject(new Error(`[deferred timeout] ${tag}`)), timeout)
timeout = setTimeout(
() => reject(new Error(`[deferred timeout] ${tag}`)),
timeout
)
}
})
return {
created: Date.now(),
setTag: (t: string) => tag = t,
resolve, reject, promise,
setTag: (t: string) => (tag = t),
resolve,
reject,
promise,
get settled() {
return settled
}
},
}
}
export function invokeHostExportedApi (
method: string,
...args: Array<any>
) {
method = method?.startsWith('_call') ? method :
method?.replace(/^[_$]+/, '')
const method1 = snakeCase(method)
export function invokeHostExportedApi(method: string, ...args: Array<any>) {
method = method?.startsWith('_call') ? method : method?.replace(/^[_$]+/, '')
const method1 = safeSnakeCase(method)
const logseqHostExportedApi = Object.assign(
// @ts-ignore
const logseqHostExportedApi = window.logseq?.api || {}
window.logseq?.api || {},
callables
)
const fn = logseqHostExportedApi[method1] || window.apis[method1] ||
logseqHostExportedApi[method] || window.apis[method]
const fn =
logseqHostExportedApi[method1] ||
window.apis[method1] ||
logseqHostExportedApi[method] ||
window.apis[method]
if (!fn) {
throw new Error(`Not existed method #${method}`)
}
return typeof fn !== 'function' ? fn : fn.apply(null, args)
return typeof fn !== 'function' ? fn : fn.apply(this, args)
}
export function setupIframeSandbox(
@@ -180,7 +189,8 @@ export function setupInjectedStyle (
el = document.createElement('style')
el.textContent = style
attrs && Object.entries(attrs).forEach(([k, v]) => {
attrs &&
Object.entries(attrs).forEach(([k, v]) => {
el.setAttribute(k, v)
})
@@ -197,7 +207,7 @@ export function setupInjectedUI (
this: PluginLocal,
ui: UIOptions,
attrs: Record<string, string>,
initialCallback?: (e: { el: HTMLElement, float: boolean }) => void
initialCallback?: (e: { el: HTMLElement; float: boolean }) => void
) {
let slot: string = ''
let selector: string
@@ -217,21 +227,32 @@ export function setupInjectedUI (
const id = `${ui.key}-${slot}-${pl.id}`
const key = `${ui.key}--${pl.id}`
const target = float ? document.body : (selector && document.querySelector(selector))
const target = float
? document.body
: selector && document.querySelector(selector)
if (!target) {
console.error(`${this.debugTag} can not resolve selector target ${selector}`)
console.error(
`${this.debugTag} can not resolve selector target ${selector}`
)
return
}
if (ui.template) {
// safe template
ui.template = DOMPurify.sanitize(
ui.template, {
ui.template = DOMPurify.sanitize(ui.template, {
ADD_TAGS: ['iframe'],
ALLOW_UNKNOWN_PROTOCOLS: true,
ADD_ATTR: ['allow', 'src', 'allowfullscreen', 'frameborder', 'scrolling', 'target']
ADD_ATTR: [
'allow',
'src',
'allowfullscreen',
'frameborder',
'scrolling',
'target',
],
})
} else { // remove ui
} else {
// remove ui
injectedUIEffects.get(id)?.call(null)
return
}
@@ -243,14 +264,17 @@ export function setupInjectedUI (
content.innerHTML = ui.template
// update attributes
attrs && Object.entries(attrs).forEach(([k, v]) => {
attrs &&
Object.entries(attrs).forEach(([k, v]) => {
el.setAttribute(k, v)
})
let positionDirty = el.dataset.dx != null
ui.style && Object.entries(ui.style).forEach(([k, v]) => {
if (positionDirty && [
'left', 'top', 'bottom', 'right', 'width', 'height'].includes(k)
ui.style &&
Object.entries(ui.style).forEach(([k, v]) => {
if (
positionDirty &&
['left', 'top', 'bottom', 'right', 'width', 'height'].includes(k)
) {
return
}
@@ -275,11 +299,13 @@ export function setupInjectedUI (
// TODO: enhance template
content.innerHTML = ui.template
attrs && Object.entries(attrs).forEach(([k, v]) => {
attrs &&
Object.entries(attrs).forEach(([k, v]) => {
el.setAttribute(k, v)
})
ui.style && Object.entries(ui.style).forEach(([k, v]) => {
ui.style &&
Object.entries(ui.style).forEach(([k, v]) => {
el.style[k] = v
})
@@ -291,33 +317,54 @@ export function setupInjectedUI (
el.setAttribute('resizable', 'true')
ui.close && (el.dataset.close = ui.close)
el.classList.add('lsp-ui-float-container', 'visible')
disposeFloat = (
pl._setupResizableContainer(el, key),
pl._setupDraggableContainer(el, { key, close: () => teardownUI(), title: attrs?.title }))
disposeFloat =
(pl._setupResizableContainer(el, key),
pl._setupDraggableContainer(el, {
key,
close: () => teardownUI(),
title: attrs?.title,
}))
}
if (!!slot && ui.reset) {
const exists = Array.from(target.querySelectorAll('[data-injected-ui]'))
.map((it: HTMLElement) => it.id)
const exists = Array.from(
target.querySelectorAll('[data-injected-ui]')
).map((it: HTMLElement) => it.id)
exists?.forEach((exist: string) => {
injectedUIEffects.get(exist)?.call(null)
})
}
target.appendChild(el);
target.appendChild(el)
// TODO: How handle events
['click', 'focus', 'focusin', 'focusout', 'blur', 'dblclick',
'keyup', 'keypress', 'keydown', 'change', 'input'].forEach((type) => {
el.addEventListener(type, (e) => {
;[
'click',
'focus',
'focusin',
'focusout',
'blur',
'dblclick',
'keyup',
'keypress',
'keydown',
'change',
'input',
].forEach((type) => {
el.addEventListener(
type,
(e) => {
const target = e.target! as HTMLElement
const trigger = target.closest(`[data-on-${type}]`) as HTMLElement
if (!trigger) return
const msgType = trigger.dataset[`on${ucFirst(type)}`]
msgType && pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
}, false)
msgType &&
pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
},
false
)
})
// callback
@@ -333,6 +380,12 @@ export function setupInjectedUI (
return teardownUI
}
export function cleanInjectedScripts(this: PluginLocal) {
const scripts = document.head.querySelectorAll(`script[data-ref=${this.id}]`)
scripts?.forEach((it) => it.remove())
}
export function transformableEvent(target: HTMLElement, e: Event) {
const obj: any = {}
@@ -340,9 +393,7 @@ export function transformableEvent (target: HTMLElement, e: Event) {
const ds = target.dataset
const FLAG_RECT = 'rect'
;['value', 'id', 'className',
'dataset', FLAG_RECT
].forEach((k) => {
;['value', 'id', 'className', 'dataset', FLAG_RECT].forEach((k) => {
let v: any
switch (k) {
@@ -389,7 +440,8 @@ export function setupInjectedTheme (url?: string) {
export function mergeSettingsWithSchema(
settings: Record<string, any>,
schema: Array<SettingSchemaDesc>) {
schema: Array<SettingSchemaDesc>
) {
const defaults = (schema || []).reduce((a, b) => {
if ('default' in b) {
a[b.key] = b.default

View File

@@ -0,0 +1,67 @@
import { LSPluginUser } from '../LSPlugin.user'
import { PluginLocal } from '../LSPlugin.core'
import { safeSnakeCase } from '../helpers'
/**
* Some experiment features
*/
export class LSPluginExperiments {
constructor(private ctx: LSPluginUser) {}
get React(): unknown {
return this.ensureHostScope().React
}
get ReactDOM(): unknown {
return this.ensureHostScope().ReactDOM
}
get pluginLocal(): PluginLocal {
return this.ensureHostScope().LSPluginCore.ensurePlugin(
this.ctx.baseInfo.id
)
}
private invokeExperMethod(type: string, ...args: Array<any>) {
const host = this.ensureHostScope()
type = safeSnakeCase(type)?.toLowerCase()
return host.logseq.api['exper_' + type]?.apply(host, args)
}
async loadScripts(...scripts: Array<string>) {
scripts = scripts.map((it) => {
if (!it?.startsWith('http')) {
return this.ctx.resolveResourceFullUrl(it)
}
return it
})
scripts.unshift(this.ctx.baseInfo.id)
await this.invokeExperMethod('loadScripts', ...scripts)
}
registerFencedCodeRenderer(
type: string,
opts: {
edit?: boolean
before?: () => Promise<void>
subs?: Array<string>
render: (props: { content: string }) => any
}
) {
return this.ensureHostScope().logseq.api.exper_register_fenced_code_renderer(
this.ctx.baseInfo.id,
type,
opts
)
}
ensureHostScope(): any {
if (window === top) {
throw new Error('Can not access host scope!')
}
return top
}
}

View File

@@ -19,9 +19,7 @@ class LSPluginFileStorage implements IAsyncStorage {
/**
* @param ctx
*/
constructor (
private ctx: LSPluginUser
) {}
constructor(private ctx: LSPluginUser) {}
/**
* plugin id
@@ -37,7 +35,7 @@ class LSPluginFileStorage implements IAsyncStorage {
setItem(key: string, value: string): Promise<void> {
return this.ctx.caller.callAsync(`api:call`, {
method: 'write-plugin-storage-file',
args: [this.ctxId, key, value]
args: [this.ctxId, key, value],
})
}
@@ -47,7 +45,7 @@ class LSPluginFileStorage implements IAsyncStorage {
getItem(key: string): Promise<string | undefined> {
return this.ctx.caller.callAsync(`api:call`, {
method: 'read-plugin-storage-file',
args: [this.ctxId, key]
args: [this.ctxId, key],
})
}
@@ -57,7 +55,7 @@ class LSPluginFileStorage implements IAsyncStorage {
removeItem(key: string): Promise<void> {
return this.ctx.caller.call(`api:call`, {
method: 'unlink-plugin-storage-file',
args: [this.ctxId, key]
args: [this.ctxId, key],
})
}
@@ -67,7 +65,7 @@ class LSPluginFileStorage implements IAsyncStorage {
clear(): Promise<void> {
return this.ctx.caller.call(`api:call`, {
method: 'clear-plugin-storage-files',
args: [this.ctxId]
args: [this.ctxId],
})
}
@@ -77,11 +75,9 @@ class LSPluginFileStorage implements IAsyncStorage {
hasItem(key: string): Promise<boolean> {
return this.ctx.caller.callAsync(`api:call`, {
method: 'exist-plugin-storage-file',
args: [this.ctxId, key]
args: [this.ctxId, key],
})
}
}
export {
LSPluginFileStorage
}
export { LSPluginFileStorage }

View File

@@ -27,7 +27,7 @@ export const generateNewMessageId = () => ++_messageId
/**
* Postmate logging function that enables/disables via config
*/
export const log = (...args) => Postmate.debug ? console.log(...args) : null
export const log = (...args) => (Postmate.debug ? console.log(...args) : null)
/**
* Takes a URL and returns the origin
@@ -38,7 +38,11 @@ export const resolveOrigin = (url) => {
const a = document.createElement('a')
a.href = url
const protocol = a.protocol.length > 4 ? a.protocol : window.location.protocol
const host = a.host.length ? ((a.port === '80' || a.port === '443') ? a.hostname : a.host) : window.location.host
const host = a.host.length
? a.port === '80' || a.port === '443'
? a.hostname
: a.host
: window.location.host
return a.origin || `${protocol}//${host}`
}
@@ -58,15 +62,11 @@ const messageTypes = {
* @return {Boolean}
*/
export const sanitize = (message, allowedOrigin) => {
if (
typeof allowedOrigin === 'string' &&
message.origin !== allowedOrigin
) return false
if (typeof allowedOrigin === 'string' && message.origin !== allowedOrigin)
return false
if (!message.data) return false
if (
typeof message.data === 'object' &&
!('postmate' in message.data)
) return false
if (typeof message.data === 'object' && !('postmate' in message.data))
return false
if (message.data.type !== messageType) return false
if (!messageTypes[message.data.postmate]) return false
return true
@@ -80,8 +80,8 @@ export const sanitize = (message, allowedOrigin) => {
* @return {Promise}
*/
export const resolveValue = (model, property) => {
const unwrappedContext = typeof model[property] === 'function'
? model[property]() : model[property]
const unwrappedContext =
typeof model[property] === 'function' ? model[property]() : model[property]
return Promise.resolve(unwrappedContext)
}
@@ -114,14 +114,14 @@ export class ParentAPI {
/**
* the assignments below ensures that e, data, and value are all defined
*/
const { data, name } = (((e || {}).data || {}).value || {})
const { data, name } = ((e || {}).data || {}).value || {}
if (e.data.postmate === 'emit') {
if (process.env.NODE_ENV !== 'production') {
log(`Parent: Received event emission: ${name}`)
}
if (name in this.events) {
this.events[name].forEach(callback => {
this.events[name].forEach((callback) => {
callback.call(this, data)
})
}
@@ -149,23 +149,29 @@ export class ParentAPI {
this.parent.addEventListener('message', transact, false)
// Then ask child for information
this.child.postMessage({
this.child.postMessage(
{
postmate: 'request',
type: messageType,
property,
uid,
}, this.childOrigin)
},
this.childOrigin
)
})
}
call(property, data) {
// Send information to the child
this.child.postMessage({
this.child.postMessage(
{
postmate: 'call',
type: messageType,
property,
data,
}, this.childOrigin)
},
this.childOrigin
)
}
on(eventName, callback) {
@@ -215,22 +221,27 @@ export class ChildAPI {
const { property, uid, data } = e.data
if (e.data.postmate === 'call') {
if (property in this.model && typeof this.model[property] === 'function') {
if (
property in this.model &&
typeof this.model[property] === 'function'
) {
this.model[property](data)
}
return
}
// Reply to Parent
resolveValue(this.model, property)
.then(value => {
(e.source as WindowProxy).postMessage({
resolveValue(this.model, property).then((value) => {
;(e.source as WindowProxy).postMessage(
{
property,
postmate: 'reply',
type: messageType,
uid,
value,
}, e.origin)
},
e.origin
)
})
})
}
@@ -239,14 +250,17 @@ export class ChildAPI {
if (process.env.NODE_ENV !== 'production') {
log(`Child: Emitting Event "${name}"`, data)
}
this.parent.postMessage({
this.parent.postMessage(
{
postmate: 'emit',
type: messageType,
value: {
name,
data,
},
}, this.parentOrigin)
},
this.parentOrigin
)
}
}
@@ -283,7 +297,10 @@ export class Postmate {
this.frame = document.createElement('iframe')
if (opts.id) this.frame.id = opts.id
if (opts.name) this.frame.name = opts.name
this.frame.classList.add.apply(this.frame.classList, opts.classListArray || [])
this.frame.classList.add.apply(
this.frame.classList,
opts.classListArray || []
)
this.container.appendChild(this.frame)
this.child = this.frame.contentWindow
this.model = opts.model || {}
@@ -330,11 +347,14 @@ export class Postmate {
if (process.env.NODE_ENV !== 'production') {
log(`Parent: Sending handshake attempt ${attempt}`, { childOrigin })
}
this.child.postMessage({
this.child.postMessage(
{
postmate: 'handshake',
type: messageType,
model: this.model,
}, childOrigin)
},
childOrigin
)
if (attempt === maxHandshakeRequests) {
clearInterval(responseInterval)
@@ -394,16 +414,19 @@ export class Model {
if (process.env.NODE_ENV !== 'production') {
log('Child: Sending handshake reply to Parent')
}
(e.source as WindowProxy).postMessage({
;(e.source as WindowProxy).postMessage(
{
postmate: 'handshake-reply',
type: messageType,
}, e.origin)
},
e.origin
)
this.parentOrigin = e.origin
// Extend model with the one provided by the parent
const defaults = e.data.model
if (defaults) {
Object.keys(defaults).forEach(key => {
Object.keys(defaults).forEach((key) => {
this.model[key] = defaults[key]
})
if (process.env.NODE_ENV !== 'production') {

View File

@@ -1,3 +1,4 @@
const pkg = require('./package.json')
const path = require('path')
const webpack = require('webpack')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
@@ -20,6 +21,9 @@ module.exports = {
new webpack.ProvidePlugin({
process: 'process/browser',
}),
new webpack.DefinePlugin({
LIB_VERSION: JSON.stringify(pkg.version)
})
// new BundleAnalyzerPlugin()
],
output: {

View File

@@ -857,6 +857,16 @@ pkg-dir@^4.2.0:
dependencies:
find-up "^4.0.0"
prettier-config-standard@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/prettier-config-standard/-/prettier-config-standard-5.0.0.tgz#c99dbef099412eda0876f75fdc1732ffef2ab0e0"
integrity sha512-QK252QwCxlsak8Zx+rPKZU31UdbRcu9iUk9X1ONYtLSO221OgvV9TlKoTf6iPDZtvF3vE2mkgzFIEgSUcGELSQ==
prettier@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032"
integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"

File diff suppressed because one or more lines are too long

View File

@@ -33,6 +33,13 @@
(log-error error))
(p/rejected error)))))))
(defn run-git2!
[commands]
(when-let [path (state/get-graph-path)]
(when (fs/existsSync path)
(p/let [^js result (.exec GitProcess commands path)]
result))))
(defn git-dir-exists?
[]
(try

View File

@@ -7,6 +7,7 @@
["path" :as path]
["os" :as os]
["diff-match-patch" :as google-diff]
["/electron/utils" :as js-utils]
[electron.fs-watcher :as watcher]
[electron.configs :as cfgs]
[promesa.core :as p]
@@ -336,6 +337,11 @@
(defmethod handle :getAppBaseInfo [^js win [_ _opts]]
{:isFullScreen (.isFullScreen win)})
(defmethod handle :getAssetsFiles [^js win [_ {:keys [exts]}]]
(when-let [graph-path (state/get-window-graph-path win)]
(p/let [^js files (js-utils/getAllFiles (.join path graph-path "assets") (clj->js exts))]
files)))
(defn close-watcher-when-orphaned!
"When it's the last window for the directory, close the watcher."
[window graph-path]
@@ -358,6 +364,11 @@
(when (seq args)
(git/raw! args)))
(defmethod handle :runGitWithinCurrentGraph [_ [_ args]]
(when (seq args)
(git/init!)
(git/run-git2! (clj->js args))))
(defmethod handle :gitCommitAll [_ [_ message]]
(git/add-all-and-commit! message))

View File

@@ -29,7 +29,8 @@
(defn fetch-latest-release-asset
[{:keys [repo theme]}]
(p/catch
(p/let [api #(str "https://api.github.com/repos/" repo "/" %)
(p/let [repo (some-> repo (string/trim) (string/replace #"^/+(.+?)/+$" "$1"))
api #(str "https://api.github.com/repos/" repo "/" %)
endpoint (api "releases/latest")
^js res (fetch endpoint)
res (.json res)
@@ -47,14 +48,15 @@
(:body res)])
(fn [^js e]
(emit :lsp-installed {:status :error :payload e})
(throw (js/Error. :release-network-issue)))))
(debug e)
(throw (js/Error. [:release-channel-issue (.-message e)])))))
(defn download-asset-zip
[{:keys [id repo title author description effect sponsors]} dl-url dl-version dot-extract-to]
(p/catch
(p/let [^js res (fetch dl-url {:timeout 30000})
_ (when-not (.-ok res) (throw (js/Error. :download-network-issue)))
_ (when-not (.-ok res)
(throw (js/Error. [:download-channel-issue (.-statusText res)])))
frm-zip (p/create
(fn [resolve1 reject1]
(let [body (.-body res)
@@ -155,7 +157,7 @@
_ (when-not dl-url
(debug "[Download URL Error]" asset)
(throw (js/Error. :release-asset-not-found)))
(throw (js/Error. [:release-asset-not-found (js/JSON.stringify asset)])))
dest (.join path cfgs/dot-root "plugins" (:id item))
_ (when-not only-check (download-asset-zip item dl-url latest-version dest))
@@ -175,7 +177,8 @@
(emit :lsp-installed
{:status :error
:only-check only-check
:payload (assoc item :error-code (.-message e))}))
:payload (assoc item :error-code (.-message e))})
(debug e))
(resolve nil)))))
(p/finally

View File

@@ -1,18 +1,56 @@
const path = require('path')
const { readdir, lstat } = require('fs').promises
// workaround from https://github.com/electron/electron/issues/426#issuecomment-658901422
// We set an intercept on incoming requests to disable x-frame-options
// headers.
export const disableXFrameOptions = (win) => {
win.webContents.session.webRequest.onHeadersReceived({ urls: [ "*://*/*" ] },
win.webContents.session.webRequest.onHeadersReceived({ urls: ['*://*/*'] },
(d, c) => {
if (d.responseHeaders['X-Frame-Options']) {
delete d.responseHeaders['X-Frame-Options'];
delete d.responseHeaders['X-Frame-Options']
} else if (d.responseHeaders['x-frame-options']) {
delete d.responseHeaders['x-frame-options'];
delete d.responseHeaders['x-frame-options']
}
c({cancel: false, responseHeaders: d.responseHeaders});
c({ cancel: false, responseHeaders: d.responseHeaders })
}
)
}
);
};
export async function getAllFiles (dir, exts) {
const dirents = await readdir(dir, { withFileTypes: true })
if (exts) {
!Array.isArray(exts) && (exts = [exts])
exts = exts.map(it => {
if (it && !it.startsWith('.')) {
it = '.' + it
}
return it?.toLowerCase()
})
}
const files = await Promise.all(dirents.map(async (dirent) => {
if (exts && !exts.includes(path.extname(dirent.name))) {
return null
}
const filePath = path.resolve(dir, dirent.name)
const fileStats = await lstat(filePath)
const stats = {
size: fileStats.size,
accessTime: fileStats.atimeMs,
modifiedTime: fileStats.mtimeMs,
changeTime: fileStats.ctimeMs,
birthTime: fileStats.birthtimeMs
}
return dirent.isDirectory() ? getAllFiles(filePath) : {
path: filePath, ...stats
}
}))
return files.flat().filter(it => it != null)
}

View File

@@ -2880,7 +2880,10 @@
[:sup.fn (str name "↩︎")]])]])
["Src" options]
(src-cp config options html-export?)
[:div.cp__fenced-code-block
(if-let [opts (plugin-handler/hook-fenced-code-by-type (util/safe-lower-case (:language options)))]
(plugins/hook-ui-fenced-code (string/join "" (:lines options)) opts)
(src-cp config options html-export?))]
:else
"")

View File

@@ -1,8 +1,10 @@
.block-content-wrapper {
/* 38px is the width of block-control */
width: calc(100% - 22px);
@screen sm {
width: calc(100% - 33px);
overflow-x: auto;
}
}
@@ -183,6 +185,14 @@
}
}
html.is-mobile,
html.is-native-iphone,
html.is-native-android {
.references .block-control {
margin-left: -20px;
}
}
.block-ref {
border-bottom: 0.5px solid;
border-bottom-color: var(--ls-block-ref-link-text-color);
@@ -357,22 +367,27 @@
.document-mode .editor-inner .h1 {
margin: 0.67em 0;
}
.document-mode .ls-block h2,
.document-mode .editor-inner .h2 {
margin: 0.75em 0;
}
.document-mode .ls-block h3,
.document-mode .editor-inner .h3 {
margin: 0.83em 0;
}
.document-mode .ls-block h4,
.document-mode .editor-inner .h4 {
margin: 1.12em 0;
}
.document-mode .ls-block h5,
.document-mode .editor-inner .h5 {
margin: 1.5em 0;
}
.document-mode .ls-block h6,
.document-mode .editor-inner .h6 {
margin: 1.67em 0;
@@ -558,6 +573,12 @@ a.cloze-revealed {
opacity: 1;
}
.cp__fenced-code-block {
.not-edit {
cursor: default;
}
}
html.is-native-ios {
audio {
width: 300px;

View File

@@ -40,7 +40,7 @@
[page]
(let [namespaces (get-relation page)]
(when (seq namespaces)
[:div.page-hierachy.mt-6
[:div.page-hierarchy.mt-6
(ui/foldable
[:h2.font-bold.opacity-30 "Hierarchy"]
[:ul.namespaces {:style {:margin "12px 24px"}}

View File

@@ -13,7 +13,6 @@
[frontend.components.plugins-settings :as plugins-settings]
[frontend.handler.notification :as notification]
[frontend.handler.plugin :as plugin-handler]
[frontend.handler.page :as page-handler]
[clojure.string :as string]))
(rum/defcs installed-themes
@@ -234,8 +233,7 @@
(ui/toggle (not disabled?)
(fn []
(js-invoke js/LSPluginCore (if disabled? "enable" "disable") id)
(page-handler/init-commands!))
(js-invoke js/LSPluginCore (if disabled? "enable" "disable") id))
true)]])
(rum/defc plugin-item-card < rum/static
@@ -316,10 +314,12 @@
[:input.form-input.is-small
{:placeholder "Search plugins"
:ref *search-ref
:auto-focus true
:on-key-down (fn [^js e]
(when (= 27 (.-keyCode e))
(when-not (string/blank? search-key)
(util/stop e)
(if (string/blank? search-key)
(some-> (js/document.querySelector ".cp__plugins-page") (.focus))
(reset! *search-key nil))))
:on-change #(let [^js target (.-target %)]
(reset! *search-key (util/trim-safe (.-value target))))
@@ -726,10 +726,7 @@
[:span])))
(rum/defcs hook-ui-items < rum/reactive
"type
- :toolbar
- :pagebar
"
"type: :toolbar, :pagebar"
[_state type]
(when (state/sub [:plugin/installed-ui-items])
(let [items (state/get-plugins-ui-items-with-type type)]
@@ -739,6 +736,15 @@
(for [[_ {:keys [key] :as opts} pid] items]
(rum/with-key (ui-item-renderer pid type opts) key))]))))
(rum/defcs hook-ui-fenced-code < rum/reactive
[_state content {:keys [render edit] :as _opts}]
[:div
{:on-mouse-down (fn [e] (when (false? edit) (util/stop e)))
:class (util/classnames [{:not-edit (false? edit)}])}
(when (fn? render)
(js/React.createElement render #js {:content content}))])
(rum/defc plugins-page
[]
@@ -746,11 +752,6 @@
market? (= active :marketplace)
*el-ref (rum/create-ref)]
(rum/use-effect!
#(let [^js el (rum/deref *el-ref)]
(js/setTimeout (fn [] (.focus el)) 100))
[])
[:div.cp__plugins-page
{:ref *el-ref
:tab-index "-1"}
@@ -775,10 +776,13 @@
(rum/defcs focused-settings-content
< rum/reactive
(rum/local (state/sub :plugin/focused-settings) ::cache)
[_state title]
(let [focused (state/sub :plugin/focused-settings)
(let [*cache (::cache _state)
focused (state/sub :plugin/focused-settings)
nav? (state/sub :plugin/navs-settings?)
_ (state/sub :plugin/installed-plugins)]
_ (state/sub :plugin/installed-plugins)
_ (js/setTimeout #(reset! *cache focused) 100)]
[:div.cp__plugins-settings.cp__settings-main
[:header
@@ -802,7 +806,8 @@
[:article
[:div.panel-wrap
(when-let [^js pl (and focused (plugin-handler/get-plugin-inst focused))]
(when-let [^js pl (and focused (= @*cache focused)
(plugin-handler/get-plugin-inst focused))]
(ui/catch-error
[:p.warning.text-lg.mt-5 "Settings schema Error!"]
(plugins-settings/settings-container

View File

@@ -720,7 +720,6 @@
&.is-dragging {
/*height: var(--ls-draggable-handle-height) !important;*/
overflow: hidden;
opacity: .7;
> .draggable-handle {

View File

@@ -3,7 +3,8 @@
[frontend.util :as util]
[frontend.ui :as ui]
[frontend.handler.plugin :as plugin-handler]
[cljs-bean.core :as bean]))
[cljs-bean.core :as bean]
[goog.functions :refer [debounce]]))
(rum/defc edit-settings-file
[pid {:keys [class]}]
@@ -26,8 +27,8 @@
[:input
{:class (util/classnames [{:form-input (not (contains? #{:color :range} input-as))}])
:type (name input-as)
:value (or val default)
:on-change #(update-setting! key (util/evalue %))}])]])
:defaultValue (or val default)
:on-change (debounce #(update-setting! key (util/evalue %)) 1000)}])]])
(rum/defc render-item-toggle
[val {:keys [key title description default]} update-setting!]
@@ -77,12 +78,11 @@
[schema ^js pl]
(let [^js _settings (.-settings pl)
pid (.-id pl)
[settings, set-settings] (rum/use-state nil)
[settings, set-settings] (rum/use-state (bean/->clj (.toJSON _settings)))
update-setting! (fn [k v] (.set _settings (name k) (bean/->js v)))]
(rum/use-effect!
(fn []
(set-settings (bean/->clj (.toJSON _settings)))
(let [on-change (fn [^js s]
(when-let [s (bean/->clj s)]
(set-settings s)))]

View File

@@ -69,9 +69,15 @@
flex: 1;
padding: 0 12px 12px;
max-height: 70vh;
min-height: 380px;
width: auto;
overflow: auto;
margin-right: -17px;
margin-bottom: -17px;
@screen md {
width: 680px;
}
}
&.no-aside {

View File

@@ -325,9 +325,12 @@
(when-let [coming (and (not downloading?)
(get-in @state/state [:plugin/updates-coming id]))]
(let [error-code (:error-code coming)
error-code (if (= error-code (str :no-new-version)) nil error-code)]
(when (or pending? (not error-code))
(notification/show!
(str "Checked: " (:title coming))
:success))
(str "[Checked]<" (:title coming) "> " error-code)
(if error-code :error :success)))))
(if (and updated? downloading?)
;; try to start consume downloading item
@@ -348,6 +351,13 @@
(when (and pending? (seq (state/all-available-coming-updates)))
(plugin/open-waiting-updates-modal!))))))
(defmethod handle :plugin/hook-db-tx [[_ {:keys [blocks tx-data tx-meta] :as payload}]]
(when-let [payload (and (seq blocks)
(merge payload {:tx-data (map #(into [] %) tx-data)
:tx-meta (dissoc tx-meta :editor-cursor)}))]
(plugin-handler/hook-plugin-db :changed payload)
(plugin-handler/hook-plugin-block-changes payload)))
(defmethod handle :backup/broken-config [[_ repo content]]
(when (and repo content)
(let [path (config/get-config-path)
@@ -367,6 +377,9 @@
:path
js/decodeURI)))
(defmethod handle :rebuild-slash-commands-list [[_]]
(page-handler/rebuild-slash-commands-list!))
(defn run!
[]
(let [chan (state/get-events-chan)]

View File

@@ -9,10 +9,12 @@
(defn show!
([content status]
(show! content status true nil))
(show! content status true nil 1500))
([content status clear?]
(show! content status clear? nil))
(show! content status clear? nil 1500))
([content status clear? uid]
(show! content status clear? uid 1500))
([content status clear? uid timeout]
(let [contents (state/get-notification-contents)
uid (or uid (keyword (util/unique-id)))]
(state/set-state! :notification/contents (assoc contents
@@ -20,6 +22,6 @@
:status status}))
(when (and clear? (not= status :error))
(js/setTimeout #(clear! uid) 1500))
(js/setTimeout #(clear! uid) (or timeout 1500)))
uid)))

View File

@@ -34,7 +34,8 @@
[goog.object :as gobj]
[lambdaisland.glogi :as log]
[promesa.core :as p]
[frontend.mobile.util :as mobile-util]))
[frontend.mobile.util :as mobile-util]
[goog.functions :refer [debounce]]))
(defn- get-directory
[journal?]
@@ -598,6 +599,9 @@
[]
(commands/init-commands! get-page-ref-text))
(def rebuild-slash-commands-list!
(debounce init-commands! 1500))
(defn template-exists?
[title]
(when title

View File

@@ -2,6 +2,7 @@
(:require [promesa.core :as p]
[rum.core :as rum]
[frontend.util :as util]
[clojure.walk :as walk]
[frontend.format.mldoc :as mldoc]
[frontend.handler.notification :as notifications]
[camel-snake-kebab.core :as csk]
@@ -19,6 +20,16 @@
(and (util/electron?)
(state/lsp-enabled?-or-theme)))
(defn- normalize-keyword-for-json
[input]
(when input
(walk/postwalk
(fn [a]
(cond
(keyword? a) (csk/->camelCase (name a))
(uuid? a) (str a)
:else a)) input)))
(defn invoke-exported-api
[type & args]
(try
@@ -84,7 +95,7 @@
(p/create
(fn [resolve]
(state/set-state! :plugin/installing mft)
(ipc/ipc "installMarketPlugin" mft)
(ipc/ipc :installMarketPlugin mft)
(resolve id)))))
(defn check-or-update-marketplace-plugin
@@ -100,20 +111,21 @@
(state/reset-all-updates-state)
(throw e))))
(fn [mfts]
(if-let [mft (some #(when (= (:id %) id) %) mfts)]
(ipc/ipc "updateMarketPlugin" (merge (dissoc pkg :logger) mft))
(throw (js/Error. (str ":not-found-in-marketplace" id))))
(let [mft (some #(when (= (:id %) id) %) mfts)]
;;TODO: (throw (js/Error. [:not-found-in-marketplace id]))
(ipc/ipc :updateMarketPlugin (merge (dissoc pkg :logger) mft)))
true))
(fn [^js e]
(error-handler "Update Error: remote error")
(error-handler e)
(state/set-state! :plugin/installing nil)
(js/console.error e)))))
(defn get-plugin-inst
[id]
(try
(js/LSPluginCore.ensurePlugin id)
(js/LSPluginCore.ensurePlugin (name id))
(catch js/Error _e
nil)))
@@ -175,7 +187,7 @@
(str (t :plugin/installed) (t :plugins) ": " name) :success)))))
:error
(let [error-code (keyword (string/replace (:error-code payload) #"^[\s\:]+" ""))
(let [error-code (keyword (string/replace (:error-code payload) #"^[\s\:\[]+" ""))
[msg type] (case error-code
:no-new-version
@@ -195,7 +207,8 @@
;; notify human tips
(notifications/show!
(str
(if (= :error type) "[Install Error]" "")
(if (= :error type) "[Error]" "")
(str "<" (:id payload) "> ")
msg) type)))
(js/console.error payload))
@@ -228,13 +241,15 @@
[pid [cmd actions]]
(when-let [pid (keyword pid)]
(when (contains? (:plugin/installed-plugins @state/state) pid)
(swap! state/state update-in [:plugin/installed-commands pid]
(swap! state/state update-in [:plugin/installed-slash-commands pid]
(fnil merge {}) (hash-map cmd (mapv #(conj % {:pid pid}) actions)))
(state/pub-event! [:rebuild-slash-commands-list])
true)))
(defn unregister-plugin-slash-command
[pid]
(swap! state/state medley/dissoc-in [:plugin/installed-commands (keyword pid)]))
(swap! state/state medley/dissoc-in [:plugin/installed-slash-commands (keyword pid)])
(state/pub-event! [:rebuild-slash-commands-list]))
(def keybinding-mode-handler-map
{:global :shortcut.handler/editor-global
@@ -292,11 +307,43 @@
[pid]
(swap! state/state assoc-in [:plugin/installed-ui-items (keyword pid)] []))
(defn register-plugin-resources
[pid type {:keys [key] :as opts}]
(when-let [pid (keyword pid)]
(when-let [type (and key (keyword type))]
(let [path [:plugin/installed-resources pid type]]
(when (contains? #{:error nil} (get-in @state/state (conj path key)))
(swap! state/state update-in path
(fnil assoc {}) key (merge opts {:pid pid}))
true)))))
(defn unregister-plugin-resources
[pid]
(when-let [pid (keyword pid)]
(swap! state/state medley/dissoc-in [:plugin/installed-resources pid])
true))
(defn unregister-plugin-themes
([pid] (unregister-plugin-themes pid true))
([pid effect]
(js/LSPluginCore.unregisterTheme (name pid) effect)))
(def *fenced-code-providers (atom #{}))
(defn register_fenced_code_renderer
[pid type {:keys [before subs render edit] :as _opts}]
(when-let [key (and type (keyword type))]
(register-plugin-resources pid :fenced-code-renderers
{:key key :edit edit :before before :subs subs :render render})
(swap! *fenced-code-providers conj pid)
#(swap! *fenced-code-providers disj pid)))
(defn hook-fenced-code-by-type
[type]
(when-let [key (and (seq @*fenced-code-providers) type (keyword type))]
(first (map #(state/get-plugin-resource % :fenced-code-renderers key)
@*fenced-code-providers))))
(defn select-a-plugin-theme
[pid]
(when-let [themes (get (group-by :pid (:plugin/installed-themes @state/state)) pid)]
@@ -374,13 +421,16 @@
(defn hook-plugin
[tag type payload plugin-id]
(when lsp-enabled?
(try
(js-invoke js/LSPluginCore
(str "hook" (string/capitalize (name tag)))
(name type)
(if (coll? payload)
(bean/->js (into {} (for [[k v] payload] [(csk/->camelCase k) (if (uuid? v) (str v) v)])))
(bean/->js (normalize-keyword-for-json payload))
payload)
(if (keyword? plugin-id) (name plugin-id) plugin-id))))
(if (keyword? plugin-id) (name plugin-id) plugin-id))
(catch js/Error e
(js/console.error "[Hook Plugin Err]" e)))))
(defn hook-plugin-app
([type payload] (hook-plugin-app type payload nil))
@@ -390,6 +440,18 @@
([type payload] (hook-plugin-editor type payload nil))
([type payload plugin-id] (hook-plugin :editor type payload plugin-id)))
(defn hook-plugin-db
([type payload] (hook-plugin-db type payload nil))
([type payload plugin-id] (hook-plugin :db type payload plugin-id)))
(defn hook-plugin-block-changes
[{:keys [blocks tx-data tx-meta]}]
(doseq [b blocks
:let [tx-data' (group-by first tx-data)
type (str "block:" (:block/uuid b))]]
(hook-plugin-db type {:block b :tx-data (get tx-data' (:db/id b)) :tx-meta tx-meta})))
(defn get-ls-dotdir-root
[]
(ipc/ipc "getLogseqDotDirRoot"))
@@ -480,7 +542,9 @@
;; commands
(unregister-plugin-slash-command pid)
(invoke-exported-api "unregister_plugin_simple_command" pid)
(unregister-plugin-ui-items pid))
(invoke-exported-api "uninstall_plugin_hook" pid)
(unregister-plugin-ui-items pid)
(unregister-plugin-resources pid))
_ (doto js/LSPluginCore
(.on "registered"

View File

@@ -12,6 +12,10 @@
[command]
(ipc/ipc "runGit" command))
(defn run-git-command2!
[command]
(ipc/ipc "runGitWithinCurrentGraph" command))
;; TODO: export to pdf/html/word
(defn run-pandoc-command!
[command]

View File

@@ -1,7 +1,12 @@
(ns frontend.loader
(:require [goog.net.jsloader :as jsloader]
[goog.html.legacyconversions :as conv]))
[goog.html.legacyconversions :as conv]
[cljs-bean.core :as bean]))
(defn load [url ok-handler]
(let [loader (jsloader/safeLoad (conv/trustedResourceUrlFromString (str url)))]
(.addCallback ^goog.net.jsloader loader ok-handler)))
(defn load
([url ok-handler] (load url ok-handler nil))
([url ok-handler opts]
(let [loader (jsloader/safeLoad
(conv/trustedResourceUrlFromString (str url))
(bean/->js opts))]
(.addCallback ^goog.net.jsloader loader ok-handler))))

View File

@@ -1,6 +1,7 @@
(ns frontend.modules.outliner.pipeline
(:require [frontend.modules.datascript-report.core :as ds-report]
[frontend.modules.outliner.file :as file]))
[frontend.modules.outliner.file :as file]
[frontend.state :as state]))
(defn updated-page-hook
[page]
@@ -8,7 +9,12 @@
(defn invoke-hooks
[tx-report]
(let [{:keys [pages]} (ds-report/get-blocks-and-pages tx-report)]
(let [{:keys [pages blocks]} (ds-report/get-blocks-and-pages tx-report)]
(doseq [p (seq pages)] (updated-page-hook p))
(when (and state/lsp-enabled? (seq blocks))
(state/pub-event! [:plugin/hook-db-tx
{:blocks blocks
:tx-data (:tx-data tx-report)
:tx-meta (:tx-meta tx-report)}]))
;; TODO: Add blocks to hooks
#_(doseq [b (seq blocks)])))

View File

@@ -33,7 +33,7 @@
:notification/show? false
:notification/content nil
:repo/cloning? false
;; :repo/loading-files? is only for github repos
;; :repo/loading-files? is only for GitHub repos
:repo/loading-files? {}
:repo/changed-files nil
:nfs/user-granted? {}
@@ -160,8 +160,10 @@
:plugin/indicator-text nil
:plugin/installed-plugins {}
:plugin/installed-themes []
:plugin/installed-commands {}
:plugin/installed-slash-commands {}
:plugin/installed-ui-items {}
:plugin/installed-resources {}
:plugin/installed-hooks {}
:plugin/simple-commands {}
:plugin/selected-theme nil
:plugin/selected-unpacked-pkg nil
@@ -1294,7 +1296,7 @@
(defn get-plugins-commands
[]
(mapcat seq (flatten (vals (:plugin/installed-commands @state)))))
(mapcat seq (flatten (vals (:plugin/installed-slash-commands @state)))))
(defn get-plugins-commands-with-type
[type]
@@ -1306,6 +1308,43 @@
(filterv #(= (keyword (first %)) (keyword type))
(apply concat (vals (:plugin/installed-ui-items @state)))))
(defn get-plugin-resources-with-type
[pid type]
(when-let [pid (and type (keyword pid))]
(get-in @state [:plugin/installed-resources pid (keyword type)])))
(defn get-plugin-resource
[pid type key]
(when-let [resources (get-plugin-resources-with-type pid type)]
(get resources key)))
(defn upt-plugin-resource
[pid type key attr val]
(when-let [resource (get-plugin-resource pid type key)]
(let [resource (assoc resource (keyword attr) val)]
(set-state!
[:plugin/installed-resources (keyword pid) (keyword type) key] resource)
resource)))
(defn install-plugin-hook
[pid hook]
(when-let [pid (keyword pid)]
(set-state!
[:plugin/installed-hooks hook]
(conj
((fnil identity #{}) (get-in @state [:plugin/installed-hooks hook]))
pid)) true))
(defn uninstall-plugin-hook
[pid hook-or-all]
(when-let [pid (keyword pid)]
(if (nil? hook-or-all)
(swap! state update :plugin/installed-hooks #(medley/map-vals (fn [ids] (disj ids pid)) %))
(when-let [coll (get-in @state [:plugin/installed-hooks hook-or-all])]
(set-state! [:plugin/installed-hooks hook-or-all] (disj coll pid))))
true))
(defn get-scheduled-future-days
[]
(let [days (:scheduled/future-days (get-config))]
@@ -1594,6 +1633,9 @@
[]
(:plugin/enabled @state))
(def lsp-enabled?
(lsp-enabled?-or-theme))
(defn consume-updates-coming-plugin
[payload updated?]
(when-let [id (keyword (:id payload))]

View File

@@ -28,6 +28,7 @@
[frontend.state :as state]
[frontend.util :as util]
[frontend.util.cursor :as cursor]
[frontend.loader :as loader]
[goog.dom :as gdom]
[lambdaisland.glogi :as log]
[medley.core :as medley]
@@ -56,6 +57,20 @@
(catch js/Error e
(js/console.error "[parse hiccup error]" e) input))))
(defn ^:export install-plugin-hook
[pid hook]
(state/install-plugin-hook pid hook))
(defn ^:export uninstall-plugin-hook
[pid hook-or-all]
(state/uninstall-plugin-hook pid hook-or-all))
(defn ^:export should-exec-plugin-hook
[pid hook]
(let [hooks (:plugin/installed-hooks @state/state)]
(or (nil? (seq hooks))
(contains? (get hooks hook) (keyword pid)))))
;; base
(defn ^:export get_state_from_store
[^js path]
@@ -78,6 +93,7 @@
:preferred-date-format (state/get-date-formatter)
:preferred-start-of-week (state/get-start-of-week)
:current-graph (state/get-current-repo)
:show-brackets (state/show-brackets?)
:me (state/get-me)}))))
(def ^:export get_current_graph
@@ -234,19 +250,22 @@
cmd (assoc cmd :key (string/replace (:key cmd) ":" "-"))
key (:key cmd)
keybinding (:keybinding cmd)
palette-cmd (and palette? (plugin-handler/simple-cmd->palette-cmd pid cmd action))]
palette-cmd (and palette? (plugin-handler/simple-cmd->palette-cmd pid cmd action))
action' #(state/pub-event! [:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}])]
;; handle simple commands
(plugin-handler/register-plugin-simple-command pid cmd action)
;; handle palette commands
(when palette-cmd
(when palette?
(palette-handler/register palette-cmd))
;; handle keybinding commands
(when-let [shortcut-args (and palette-cmd keybinding
(plugin-handler/simple-cmd-keybinding->shortcut-args pid key keybinding))]
(let [dispatch-cmd (fn [_ _e] (palette-handler/invoke-command palette-cmd))
(when-let [shortcut-args (and keybinding (plugin-handler/simple-cmd-keybinding->shortcut-args pid key keybinding))]
(let [dispatch-cmd (fn [_e]
(if palette?
(palette-handler/invoke-command palette-cmd)
(action')))
[handler-id id shortcut-map] (update shortcut-args 2 assoc :fn dispatch-cmd)]
(js/console.debug :shortcut/register-shortcut [handler-id id shortcut-map])
(st/register-shortcut! handler-id id shortcut-map)))))))
@@ -509,27 +528,29 @@
(get_block (:db/id block) opts))))
(def ^:export get_previous_sibling_block
(fn [uuid]
(when-let [block (db-model/query-block-by-uuid uuid)]
(fn [block-uuid]
(when-let [block (db-model/query-block-by-uuid block-uuid)]
(let [{:block/keys [parent left]} block
block (when-not (= parent left) (db-utils/pull (:db/id left)))]
(and block (bean/->js (normalize-keyword-for-json block)))))))
(def ^:export get_next_sibling_block
(fn [uuid]
(when-let [block (db-model/query-block-by-uuid uuid)]
(fn [block-uuid]
(when-let [block (db-model/query-block-by-uuid block-uuid)]
(when-let [right-siblings (outliner/get-right-siblings (outliner/->Block block))]
(bean/->js (normalize-keyword-for-json (:data (first right-siblings))))))))
(def ^:export set_block_collapsed
(fn [uuid ^js opts]
(when-let [block (db-model/get-block-by-uuid uuid)]
(fn [block-uuid ^js opts]
(when-let [block (db-model/get-block-by-uuid block-uuid)]
(let [{:keys [flag]} (bean/->clj opts)
block-uuid (uuid block-uuid)
flag (if (= "toggle" flag)
(not (util/collapsed? block))
(boolean flag))]
(if flag (editor-handler/collapse-block! uuid)
(editor-handler/expand-block! uuid))))))
(if flag (editor-handler/collapse-block! block-uuid)
(editor-handler/expand-block! block-uuid))
nil))))
(def ^:export upsert_block_property
(fn [block-uuid key value]
@@ -566,6 +587,76 @@
blocks (normalize-keyword-for-json blocks)]
(bean/->js blocks)))))
(defn ^:export get_page_linked_references
[page-name-or-uuid]
(when-let [page (and page-name-or-uuid (db-model/get-page page-name-or-uuid))]
(let [page-name (:block/name page)
ref-blocks (if page-name
(db-model/get-page-referenced-blocks page-name)
(db-model/get-block-referenced-blocks (:block/uuid page)))
ref-blocks (and (seq ref-blocks) (into [] ref-blocks))]
(bean/->js (normalize-keyword-for-json ref-blocks)))))
(defn ^:export get_pages_from_namespace
[ns]
(when-let [repo (and ns (state/get-current-repo))]
(when-let [pages (db-model/get-namespace-pages repo ns)]
(bean/->js (normalize-keyword-for-json pages)))))
(defn ^:export get_pages_tree_from_namespace
[ns]
(when-let [repo (and ns (state/get-current-repo))]
(when-let [pages (db-model/get-namespace-hierarchy repo ns)]
(bean/->js (normalize-keyword-for-json pages)))))
(defn first-child-of-block
[block]
(when-let [children (:block/_parent block)]
(first (db-model/sort-by-left children block))))
(defn second-child-of-block
[block]
(when-let [children (:block/_parent block)]
(second (db-model/sort-by-left children block))))
(defn last-child-of-block
[block]
(when-let [children (:block/_parent block)]
(last (db-model/sort-by-left children block))))
(defn ^:export prepend_block_in_page
[uuid-or-page-name content ^js opts]
(let [page? (not (util/uuid-string? uuid-or-page-name))
page-not-exist? (and page? (nil? (db-model/get-page uuid-or-page-name)))
_ (and page-not-exist? (page-handler/create! uuid-or-page-name
{:redirect? false
:create-first-block? true
:format (state/get-preferred-format)}))]
(when-let [block (db-model/get-page uuid-or-page-name)]
(let [block' (if page? (second-child-of-block block) (first-child-of-block block))
sibling? (and page? (not (nil? block')))
opts (bean/->clj opts)
opts (merge opts {:isPageBlock (and page? (not sibling?)) :sibling sibling? :before sibling?})
src (if sibling? (str (:block/uuid block')) uuid-or-page-name)]
(insert_block src content (bean/->js opts))))))
(defn ^:export append_block_in_page
[uuid-or-page-name content ^js opts]
(let [page? (not (util/uuid-string? uuid-or-page-name))
page-not-exist? (and page? (nil? (db-model/get-page uuid-or-page-name)))
_ (and page-not-exist? (page-handler/create! uuid-or-page-name
{:redirect? false
:create-first-block? true
:format (state/get-preferred-format)}))]
(when-let [block (db-model/get-page uuid-or-page-name)]
(let [block' (last-child-of-block block)
sibling? (not (nil? block'))
opts (bean/->clj opts)
opts (merge opts {:isPageBlock (and page? (not sibling?)) :sibling sibling?}
(when sibling? {:before false}))
src (if sibling? (str (:block/uuid block')) uuid-or-page-name)]
(insert_block src content (bean/->js opts))))))
;; plugins
(def ^:export __install_plugin
(fn [^js manifest]
@@ -612,13 +703,79 @@
(when-let [args (and args (seq (bean/->clj args)))]
(shell/run-git-command! args)))
;; helpers
(defn ^:export show_msg
([content] (show_msg content :success))
([content status] (let [hiccup? (and (string? content) (string/starts-with? (string/triml content) "[:"))
content (if hiccup? (parse-hiccup-ui content) content)]
(notification/show! content (keyword status)))))
;; git
(defn ^:export git_exec_command
[^js args]
(when-let [args (and args (seq (bean/->clj args)))]
(shell/run-git-command2! args)))
(defn ^:export git_load_ignore_file
[]
(when-let [repo (state/get-current-repo)]
(p/let [file ".gitignore"
dir (config/get-repo-dir repo)
_ (fs/create-if-not-exists repo dir file)
content (fs/read-file dir file)]
content)))
(defn ^:export git_save_ignore_file
[content]
(when-let [repo (and (string? content) (state/get-current-repo))]
(p/let [file ".gitignore"
dir (config/get-repo-dir repo)
_ (fs/write-file! repo dir file content {:skip-compare? true})])))
;; ui
(defn ^:export show_msg
([content] (show_msg content :success nil))
([content status ^js opts]
(let [{:keys [key timeout]} (bean/->clj opts)
hiccup? (and (string? content) (string/starts-with? (string/triml content) "[:"))
content (if hiccup? (parse-hiccup-ui content) content)
uid (when (string? key) (keyword key))
clear? (not= timeout 0)
key' (notification/show! content (keyword status) clear? uid timeout)]
(name key'))))
(defn ^:export ui_show_msg
[& args]
(apply show_msg args))
(defn ^:export ui_close_msg
[key]
(when (string? key)
(notification/clear! (keyword key)) nil))
;; assets
(defn ^:export assets_list_files_of_current_graph
[^js exts]
(p/let [files (ipc/ipc :getAssetsFiles {:exts exts})]
(bean/->js files)))
;; experiments
(defn ^:export exper_load_scripts
[pid & scripts]
(when-let [^js _pl (plugin-handler/get-plugin-inst pid)]
(doseq [s scripts
:let [upt-status #(state/upt-plugin-resource pid :scripts s :status %)
init? (plugin-handler/register-plugin-resources pid :scripts {:key s :src s})]]
(when init?
(p/catch
(p/then
(do
(upt-status :pending)
(loader/load s nil {:attributes {:data-ref (name pid)}}))
#(upt-status :done))
#(upt-status :error))))))
(defn ^:export exper_register_fenced_code_renderer
[pid type ^js opts]
(when-let [^js _pl (plugin-handler/get-plugin-inst pid)]
(plugin-handler/register_fenced_code_renderer
(keyword pid) type (reduce #(assoc %1 %2 (aget opts (name %2))) {}
[:edit :before :subs :render]))))
;; helpers
(defn ^:export query_element_by_id
[id]
(let [^js el (gdom/getElement id)]