Enhance/plugin APIs (#7555)

* feat: WIP native cli command support

* Add :shell/command-whitelist option

* Integrate cli to code block

* Add :code-block/command-whitelist option

* fix: size of icon

* improve(shell): cache user shell whitelist on application configures file

* improve(electron): promisify run cli command

* chore(libs): update version

* fix(plugin): incorrect payload of pdf highlights section hook

* improve(plugin): block renderer with specific block uuid

* improve(plugin): expose logger for user lib

* improve(plugin): block hooks type

* improve(plugin): block slot hook with specific block

* improve(plugin): auto generate key for provide UI options

* improve(plugin): style of injected ui container

* improve(plugin): types

* improve(plugin): async messaging api from host to plugin

* improve(plugin): add types

* improve(apis): get external plugin metadata

* improve(apis): invoke external plugin impls

* improve(apis): call external plugin impls for simple commands

* enhance(apis): datascript query api for predicate inputs

* enhance(apis): datascript query api for predicate inputs

* fix(apis): redundant args of datascript query api

* enhance(plugins): position of float ui container

* enhance(plugins): style of setting options

* enhance(plugins): layouts data for float ui

* chore(plugins): update CHANGELOG.md

* improve(apis): add types

* chore: fix some inclusive terms

* improve(apis): types

* chore(plugins): update CHANGELOG.md

* chore(plugins): build libs

* chore: update CHANGELOG.md

* chore: remove experiemental alda integration

* fix(lint): remove unused methods

Co-authored-by: Tienson Qin <tiensonqin@gmail.com>
Co-authored-by: Andelf <andelf@gmail.com>
This commit is contained in:
Charlie
2022-12-19 20:23:25 +08:00
committed by GitHub
parent dfe1d65f31
commit 020317911f
28 changed files with 781 additions and 417 deletions

View File

@@ -3,8 +3,27 @@
All notable changes to this project will be documented in this file.
## [Unreleased]
## [0.0.13]
## [0.0.11]
### Added
- Support block content slot hook `App.onBlockRendererSlotted` with a specific block UUID
- Support plugins calling each other `App.invokeExternalPlugin` with key of models & commands.
E.g. (It is recommended that the caller plugin upgrade the SDK to the latest.)
```typescript
// Defined at https://github.com/xyhp915/logseq-journals-calendar/blob/main/src/main.js#L74
await logseq.App.invokeExternalPlugin('logseq-journals-calendar.models.goToToday')
// Defined at https://github.com/vipzhicheng/logseq-plugin-vim-shortcuts/blob/bec05aeee8/src/keybindings/down.ts#L20
await logseq.App.invokeExternalPlugin('logseq-plugin-vim-shortcuts.commands.vim-shortcut-down-0')
```
- Support api of `Editor.saveFocusedCodeEditorContent` [#FQ](https://github.com/logseq/logseq/issues/7714)
- Support predicate for `DB.datascriptQuery` inputs
### Fixed
- Incorrect hook payload from `Editor.registerHighlightContextMenuItem`
- Auto generate key if not exist for `provideUI` options
## [0.0.12]
### Added

View File

@@ -1,6 +1,6 @@
{
"name": "@logseq/libs",
"version": "0.0.11",
"version": "0.0.13",
"description": "Logseq SDK libraries",
"main": "dist/lsplugin.user.js",
"typings": "index.d.ts",

View File

@@ -38,7 +38,7 @@ class LSPluginCaller extends EventEmitter {
payload: any,
actor?: DeferredActor
) => Promise<any>
private _callUserModel?: (type: string, payload: any) => Promise<any>
private _callUserModel?: (type: string, ...payloads: any[]) => Promise<any>
private _debugTag = ''
@@ -205,8 +205,13 @@ class LSPluginCaller extends EventEmitter {
return this._call?.call(this, type, payload, actor)
}
async callUserModel(type: string, payload: any = {}) {
return this._callUserModel?.call(this, type, payload)
async callUserModel(type: string, ...args: any[]) {
return this._callUserModel?.apply(this, [type, ...args])
}
async callUserModelAsync(type: string, ...args: any[]) {
type = AWAIT_LSPMSGFn(type)
return this._callUserModel?.apply(this, [type, ...args])
}
// run in host
@@ -235,12 +240,21 @@ class LSPluginCaller extends EventEmitter {
const mainLayoutInfo = (await this._pluginLocal._loadLayoutsData())?.$$0
if (mainLayoutInfo) {
cnt.dataset.inited_layout = 'true'
const { width, height, left, top } = mainLayoutInfo
let { width, height, left, top, vw, vh } = mainLayoutInfo
left = Math.max(left, 0)
left = (typeof vw === 'number') ?
`${Math.min(left * 100 / vw, 99)}%` : `${left}px`
// 45 is height of headbar
top = Math.max(top, 45)
top = (typeof vh === 'number') ?
`${Math.min(top * 100 / vh, 99)}%` : `${top}px`
Object.assign(cnt.style, {
width: width + 'px',
height: height + 'px',
left: left + 'px',
top: top + 'px',
left, top
})
}
} catch (e) {
@@ -292,12 +306,15 @@ class LSPluginCaller extends EventEmitter {
})
}
this._callUserModel = async (type, payload: any) => {
this._callUserModel = async (type, ...payloads: any[]) => {
if (type.startsWith(FLAG_AWAIT)) {
// TODO: attach payload with method call
return await refChild.get(type.replace(FLAG_AWAIT, ''))
// TODO: attach arguments with method call
return await refChild.get(
type.replace(FLAG_AWAIT, ''),
...payloads
)
} else {
refChild.call(type, payload)
refChild.call(type, payloads?.[0])
}
}

View File

@@ -19,7 +19,7 @@ import {
cleanInjectedScripts,
safeSnakeCase,
injectTheme,
cleanInjectedUI,
cleanInjectedUI, PluginLogger,
} from './helpers'
import * as pluginHelpers from './helpers'
import Debug from 'debug'
@@ -132,59 +132,6 @@ class PluginSettings extends EventEmitter<'change' | 'reset'> {
}
}
class PluginLogger extends EventEmitter<'change'> {
private _logs: Array<[type: string, payload: any]> = []
constructor(private readonly _tag: string) {
super()
}
write(type: string, payload: any[], inConsole?: boolean) {
if (payload?.length && (true === payload[payload.length - 1])) {
inConsole = true
payload.pop()
}
const msg = payload.reduce((ac, it) => {
if (it && it instanceof Error) {
ac += `${it.message} ${it.stack}`
} else {
ac += it.toString()
}
return ac
}, `[${this._tag}][${new Date().toLocaleTimeString()}] `)
this._logs.push([type, msg])
if (inConsole) {
console?.['ERROR' === type ? 'error' : 'debug'](`${type}: ${msg}`)
}
this.emit('change')
}
clear() {
this._logs = []
this.emit('change')
}
info(...args: any[]) {
this.write('INFO', args)
}
error(...args: any[]) {
this.write('ERROR', args)
}
warn(...args: any[]) {
this.write('WARN', args)
}
toJSON() {
return this._logs
}
}
interface UserPreferences {
theme: LegacyTheme
themes: {
@@ -204,7 +151,6 @@ interface PluginLocalOptions {
mode: 'shadow' | 'iframe'
settingsSchema?: SettingSchemaDesc[]
settings?: PluginSettings
logger?: PluginLogger
effect?: boolean
theme?: boolean
@@ -460,6 +406,7 @@ class PluginLocal extends EventEmitter<'loaded'
private _localRoot?: string
private _dotSettingsFile?: string
private _caller?: LSPluginCaller
private _logger?: PluginLogger
/**
* @param _options
@@ -483,7 +430,7 @@ class PluginLocal extends EventEmitter<'loaded'
async _setupUserSettings(reload?: boolean) {
const { _options } = this
const logger = (_options.logger = new PluginLogger('Loader'))
const logger = (this._logger = new PluginLogger('Loader'))
if (_options.settings && !reload) {
return
@@ -1056,13 +1003,17 @@ class PluginLocal extends EventEmitter<'loaded'
}
get logger() {
return this.options.logger
return this._logger
}
get disabled() {
return this.settings?.get('disabled')
}
get theme() {
return this.options.theme
}
get caller() {
return this._caller
}
@@ -1123,6 +1074,8 @@ class PluginLocal extends EventEmitter<'loaded'
json.usf = this.dotSettingsFile
json.iir = this.isInstalledInDotRoot
json.lsr = this._resolveResourceFullUrl('/')
json.settings = json.settings?.toJSON()
return json
}
}
@@ -1535,6 +1488,16 @@ class LSPluginCore
return this._registeredThemes
}
get enabledPlugins() {
return [...this.registeredPlugins.entries()].reduce((a, b) => {
let p = b?.[1]
if (p?.disabled !== true) {
a.set(b?.[0], p)
}
return a
}, new Map())
}
async registerTheme(id: PluginLocalIdentity, opt: Theme): Promise<void> {
debug('Register theme #', id, opt)

View File

@@ -102,6 +102,10 @@ export type IUserHook<E = any, R = IUserOffHook> = (
export type IUserSlotHook<E = any> = (
callback: (e: IHookEvent & UISlotIdentity & E) => void
) => void
export type IUserConditionSlotHook<C = any, E = any> = (
condition: C,
callback: (e: IHookEvent & UISlotIdentity & E) => void
) => void
export type EntityID = number
export type BlockUUID = string
@@ -366,11 +370,33 @@ export interface IAppProxy {
action: SimpleCommandCallback
) => void
/**
* Supported all registered palette commands
* @param type
* @param args
*/
invokeExternalCommand: (
type: ExternalCommandType,
...args: Array<any>
) => Promise<void>
/**
* Call external plugin command provided by models or registerd commands
* @added 0.0.13
* @param type `xx-plugin-id.commands.xx-key`, `xx-plugin-id.models.xx-key`
* @param args
*/
invokeExternalPlugin: (
type: string,
...args: Array<any>
) => Promise<unknown>
/**
* @added 0.0.13
* @param pid
*/
getExternalPlugin: (pid: string) => Promise<{} | null>
/**
* Get state from app store
* valid state is here
@@ -455,7 +481,13 @@ export interface IAppProxy {
onGraphAfterIndexed: IUserHook<{ repo: string }>
onThemeModeChanged: IUserHook<{ mode: 'dark' | 'light' }>
onThemeChanged: IUserHook<Partial<{ name: string, mode: string, pid: string, url: string }>>
onBlockRendererSlotted: IUserSlotHook<{ uuid: BlockUUID }>
/**
* provide ui slot to specific block with UUID
*
* @added 0.0.13
*/
onBlockRendererSlotted: IUserConditionSlotHook<BlockUUID, Omit<BlockEntity, 'children' | 'page'>>
/**
* provide ui slot to block `renderer` macro for `{{renderer arg1, arg2}}`
@@ -636,7 +668,7 @@ export interface IEditorProxy extends Record<string, any> {
/**
* @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-reddit-hot-news
*
*
* `keepUUID` will allow you to set a custom UUID for blocks by setting their properties.id
*/
insertBatchBlock: (
@@ -722,6 +754,8 @@ export interface IEditorProxy extends Record<string, any> {
editBlock: (srcBlock: BlockIdentity, opts?: { pos: number }) => Promise<void>
selectBlock: (srcBlock: BlockIdentity) => Promise<void>
saveFocusedCodeEditorContent: () => Promise<void>
upsertBlockProperty: (
block: BlockIdentity,
key: string,

View File

@@ -1,6 +1,8 @@
import {
isValidUUID,
deepMerge,
mergeSettingsWithSchema,
PluginLogger,
safeSnakeCase,
safetyPathJoin,
} from './helpers'
@@ -55,6 +57,7 @@ type callableMethods =
const PROXY_CONTINUE = Symbol.for('proxy-continue')
const debug = Debug('LSPlugin:user')
const logger = new PluginLogger('', { console: true })
/**
* @param type (key of group commands)
@@ -87,12 +90,26 @@ function registerSimpleCommand(
method: 'register-plugin-simple-command',
args: [
this.baseInfo.id,
// [cmd, action]
[{ key, 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()
@@ -188,6 +205,45 @@ const app: Partial<IAppProxy> = {
)
},
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)
@@ -328,6 +384,8 @@ const db: Partial<IDBProxy> = {
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 }) => {
@@ -346,6 +404,24 @@ const db: Partial<IDBProxy> = {
this.App._uninstallPluginHook(pid, hook)
}
},
datascriptQuery<T = any>(
this: LSPluginUser,
query: string,
...inputs: Array<any>
): Promise<T> {
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`,
...inputs
)
}
}
const git: Partial<IGitProxy> = {}
@@ -454,6 +530,13 @@ export class LSPluginUser
baseInfo = deepMerge(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,
@@ -464,11 +547,6 @@ export class LSPluginUser
await this.useSettingsSchema(this._settingsSchema)
}
if (baseInfo?.id) {
this._debugTag =
this._caller.debugTag = `#${baseInfo.id} [${baseInfo.name}]`
}
try {
await this._execCallableAPIAsync('setSDKMetadata', {
version: this._version,
@@ -605,6 +683,14 @@ export class LSPluginUser
return this._baseInfo
}
get effect(): Boolean {
return checkEffect(this)
}
get logger() {
return logger
}
get settings() {
return this.baseInfo?.settings
}

View File

@@ -5,6 +5,7 @@ import DOMPurify from 'dompurify'
import { merge } from 'lodash-es'
import { snakeCase } from 'snake-case'
import * as callables from './callable.apis'
import EventEmitter from 'eventemitter3'
declare global {
interface Window {
@@ -53,6 +54,74 @@ export function isObject(item: any) {
export const deepMerge = merge
export class PluginLogger extends EventEmitter<'change'> {
private _logs: Array<[type: string, payload: any]> = []
constructor(
private _tag?: string,
private _opts?: {
console: boolean
}
) {
super()
}
write(type: string, payload: any[], inConsole?: boolean) {
if (payload?.length && (true === payload[payload.length - 1])) {
inConsole = true
payload.pop()
}
const msg = payload.reduce((ac, it) => {
if (it && it instanceof Error) {
ac += `${it.message} ${it.stack}`
} else {
ac += it.toString()
}
return ac
}, `[${this._tag}][${new Date().toLocaleTimeString()}] `)
this._logs.push([type, msg])
if (inConsole || this._opts?.console) {
console?.['ERROR' === type ? 'error' : 'debug'](`${type}: ${msg}`)
}
this.emit('change')
}
clear() {
this._logs = []
this.emit('change')
}
info(...args: any[]) {
this.write('INFO', args)
}
error(...args: any[]) {
this.write('ERROR', args)
}
warn(...args: any[]) {
this.write('WARN', args)
}
setTag(s: string) {
this._tag = s
}
toJSON() {
return this._logs
}
}
export function isValidUUID(s: string) {
return (typeof s === 'string' &&
(s.length === 36) &&
(/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi).test(s))
}
export function genID() {
// Math.random should be unique because of its seeding algorithm.
// Convert it to base 36 (numbers + letters), and grab the first 9 characters
@@ -190,9 +259,9 @@ export function setupInjectedStyle(
el.textContent = style
attrs &&
Object.entries(attrs).forEach(([k, v]) => {
el.setAttribute(k, v)
})
Object.entries(attrs).forEach(([k, v]) => {
el.setAttribute(k, v)
})
document.head.append(el)
@@ -227,7 +296,7 @@ export function setupInjectedUI(
float = true
}
const id = `${pl.id}--${ui.key}`
const id = `${pl.id}--${ui.key || genID()}`
const key = id
const target = float
@@ -268,22 +337,22 @@ export function setupInjectedUI(
// update attributes
attrs &&
Object.entries(attrs).forEach(([k, v]) => {
el.setAttribute(k, v)
})
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)
) {
return
}
Object.entries(ui.style).forEach(([k, v]) => {
if (
positionDirty &&
['left', 'top', 'bottom', 'right', 'width', 'height'].includes(k)
) {
return
}
el.style[k] = v
})
el.style[k] = v
})
return
}
@@ -303,18 +372,19 @@ export function setupInjectedUI(
content.innerHTML = ui.template
attrs &&
Object.entries(attrs).forEach(([k, v]) => {
el.setAttribute(k, v)
})
Object.entries(attrs).forEach(([k, v]) => {
el.setAttribute(k, v)
})
ui.style &&
Object.entries(ui.style).forEach(([k, v]) => {
el.style[k] = v
})
Object.entries(ui.style).forEach(([k, v]) => {
el.style[k] = v
})
let teardownUI: () => void
let disposeFloat: () => void
// seu up float container
if (float) {
el.setAttribute('draggable', 'true')
el.setAttribute('resizable', 'true')
@@ -322,11 +392,11 @@ export function setupInjectedUI(
el.classList.add('lsp-ui-float-container', 'visible')
disposeFloat =
(pl._setupResizableContainer(el, key),
pl._setupDraggableContainer(el, {
key,
close: () => teardownUI(),
title: attrs?.title,
}))
pl._setupDraggableContainer(el, {
key,
close: () => teardownUI(),
title: attrs?.title,
}))
}
if (!!slot && ui.reset) {
@@ -364,7 +434,7 @@ export function setupInjectedUI(
const msgType = trigger.dataset[`on${ucFirst(type)}`]
msgType &&
pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
},
false
)

View File

@@ -79,9 +79,9 @@ export const sanitize = (message, allowedOrigin) => {
* passed to functions in the child model
* @return {Promise}
*/
export const resolveValue = (model, property) => {
export const resolveValue = (model, property, args) => {
const unwrappedContext =
typeof model[property] === 'function' ? model[property]() : model[property]
typeof model[property] === 'function' ? model[property].apply(null, args) : model[property]
return Promise.resolve(unwrappedContext)
}
@@ -134,7 +134,7 @@ export class ParentAPI {
}
}
get(property) {
get(property, ...args) {
return new Promise((resolve) => {
// Extract data from response and kill listeners
const uid = generateNewMessageId()
@@ -154,6 +154,7 @@ export class ParentAPI {
postmate: 'request',
type: messageType,
property,
args,
uid,
},
this.childOrigin
@@ -218,7 +219,7 @@ export class ChildAPI {
log('Child: Received request', e.data)
}
const { property, uid, data } = e.data
const { property, uid, data, args } = e.data
if (e.data.postmate === 'call') {
if (
@@ -231,7 +232,7 @@ export class ChildAPI {
}
// Reply to Parent
resolveValue(this.model, property).then((value) => {
resolveValue(this.model, property, args).then((value) => {
;(e.source as WindowProxy).postMessage(
{
property,
@@ -375,7 +376,6 @@ export class Postmate {
})
}
destroy() {
if (process.env.NODE_ENV !== 'production') {
log('Postmate: Destroying Postmate instance')

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -39,7 +39,8 @@
"posthog-js": "1.10.2",
"@logseq/rsapi": "0.0.57",
"electron-deeplink": "1.0.10",
"abort-controller": "3.0.0"
"abort-controller": "3.0.0",
"command-exists": "1.2.9"
},
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.57",

View File

@@ -17,8 +17,7 @@
(let [body (.toString (.readFileSync fs cfg-path))]
(if (seq body) (reader/read-string body) {}))
(catch :default e
(js/console.error :cfg-error e)
{})))
(js/console.error :cfg-error e))))
(defn- write-cfg!
[cfg]
@@ -29,9 +28,9 @@
(defn set-item!
[k v]
(let [cfg (ensure-cfg)
cfg (assoc cfg k v)]
(write-cfg! cfg)))
(when-let [cfg (ensure-cfg)]
(some->> (assoc cfg k v)
(write-cfg!))))
(defn get-item
[k]

View File

@@ -11,6 +11,7 @@
["diff-match-patch" :as google-diff]
["/electron/utils" :as js-utils]
["abort-controller" :as AbortController]
[electron.shell :as shell]
[electron.fs-watcher :as watcher]
[electron.configs :as cfgs]
[promesa.core :as p]
@@ -410,11 +411,12 @@
(defmethod handle :userAppCfgs [_window [_ k v]]
(let [config (cfgs/get-config)]
(if-not k
config
(if-let [k (and k (keyword k))]
(if-not (nil? v)
(cfgs/set-item! (keyword k) v)
(cfgs/get-item (keyword k))))))
(do (cfgs/set-item! k v)
(state/set-state! [:config k] v))
(cfgs/get-item k))
config)))
(defmethod handle :getDirname [_]
js/__dirname)
@@ -457,6 +459,24 @@
(git/init!)
(git/run-git2! (clj->js args))))
(defmethod handle :runCli [window [_ {:keys [command args returnResult]}]]
(try
(let [on-data-handler (fn [message]
(let [result (str "Running " command ": " message)]
(when returnResult
(utils/send-to-renderer window "notification"
{:type "success"
:payload result}))))
deferred (p/deferred)
on-exit-handler (fn [code]
(p/resolve! deferred code))
_job (shell/run-command-safety! command args on-data-handler on-exit-handler)]
deferred)
(catch js/Error e
(utils/send-to-renderer window "notification"
{:type "error"
:payload (.-message e)}))))
(defmethod handle :gitCommitAll [_ [_ message]]
(git/add-all-and-commit! message))

View File

@@ -0,0 +1,53 @@
(ns electron.shell
(:require
[clojure.string :as string]
[electron.state :as state]
[clojure.set :as set]
[electron.logger :as logger]
["child_process" :as child-process]
["command-exists" :as command-exists]))
(def commands-allowlist
#{"git" "pandoc" "ag" "grep" "alda"})
;(def commands-denylist
; #{"rm" "mv" "rename" "dd" ">" "command" "sudo"})
(defn- get-commands-allowlist
[]
(set/union (set (some->> (map #(some-> % str string/trim string/lower-case)
(get-in @state/state [:config :commands-allowlist]))
(remove nil?)))
commands-allowlist))
(defn- run-command!
[command args on-data on-exit]
(logger/debug "Shell: " (str command " " args))
(let [job (child-process/spawn (str command " " args)
#js []
#js {:shell true :detached false})]
(.on (.-stderr job) "data" on-data)
(.on (.-stdout job) "data" on-data)
(.on job "close" on-exit)
job))
(defn- ensure-command-exists
[command]
(when-not
(some->> command (.sync command-exists))
(throw (js/Error. (str "Shell: " command " not exist!")))) command)
(defn- ensure-command-in-allowlist
[command]
(when-not
(some->> command (contains? (get-commands-allowlist)))
(throw (js/Error. (str "Shell: " command " not be allowed!")))) command)
(defn run-command-safety!
[command args on-data on-exit]
(when (some-> command str string/trim string/lower-case
(ensure-command-exists)
(ensure-command-in-allowlist))
(run-command! command args on-data on-exit)))

View File

@@ -3,10 +3,10 @@
["electron" :refer [app BrowserWindow]]
["fs-extra" :as fs]
["path" :as path]
[cljs-bean.core :as bean]
[clojure.string :as string]
[electron.configs :as cfgs]
[electron.logger :as logger]
[cljs-bean.core :as bean]
[promesa.core :as p]))
(defonce *win (atom nil)) ;; The main window

View File

@@ -294,7 +294,7 @@
;; Allow user to modify or extend, should specify how to extend.
(state/get-commands)
(state/get-plugins-commands))
(state/get-plugins-slash-commands))
(remove nil?)
(util/distinct-by-last-wins first)))

View File

@@ -2236,6 +2236,7 @@
(let [{:block/keys [title body] :as block} (if (:block/title block) block
(merge block (block/parse-title-and-body uuid format pre-block? content)))
collapsed? (util/collapsed? block)
plugin-slotted? (and config/lsp-enabled? (state/slot-hook-exist? uuid))
block-ref? (:block-ref? config)
stop-events? (:stop-events? config)
block-ref-with-title? (and block-ref? (seq title))
@@ -2274,13 +2275,14 @@
[:div.warning.text-sm
"Large block will not be editable or searchable to not slow down the app, please use another editor to edit this block."])
[:div.flex.flex-row.justify-between.block-content-inner
[:div.flex-1.w-full
(cond
(seq title)
(build-block-title config block)
(when-not plugin-slotted?
[:div.flex-1.w-full
(cond
(seq title)
(build-block-title config block)
:else
nil)]
:else
nil)])
(clock-summary-cp block body)]
@@ -2306,18 +2308,24 @@
(not= block-type :whiteboard-shape))
(properties-cp config block))
(let [title-collapse-enabled? (:outliner/block-title-collapse-enabled? (state/get-config))]
(when (and (not block-ref-with-title?)
(seq body)
(or (not title-collapse-enabled?)
(and title-collapse-enabled? (not collapsed?))))
[:div.block-body
;; TODO: consistent id instead of the idx (since it could be changed later)
(let [body (block/trim-break-lines! (:block/body block))]
(for [[idx child] (medley/indexed body)]
(when-let [block (markup-element-cp config child)]
(rum/with-key (block-child block)
(str uuid "-" idx)))))]))
(if plugin-slotted?
[:div.block-slotted-body
(plugins/hook-block-slot
:block-content-slotted
(-> block (dissoc :block/children :block/page)))]
(let [title-collapse-enabled? (:outliner/block-title-collapse-enabled? (state/get-config))]
(when (and (not block-ref-with-title?)
(seq body)
(or (not title-collapse-enabled?)
(and title-collapse-enabled? (not collapsed?))))
[:div.block-body
;; TODO: consistent id instead of the idx (since it could be changed later)
(let [body (block/trim-break-lines! (:block/body block))]
(for [[idx child] (medley/indexed body)]
(when-let [block (markup-element-cp config child)]
(rum/with-key (block-child block)
(str uuid "-" idx)))))])))
(case (:block/warning block)
:multiple-blocks
@@ -2378,7 +2386,8 @@
string/trim
block-ref/block-ref?)]
(if (and edit? editor-box)
[:div.editor-wrapper {:id editor-id}
[:div.editor-wrapper
{:id editor-id}
(ui/catch-error
(ui/block-error "Something wrong in the editor" {})
(editor-box {:block block

View File

@@ -934,17 +934,15 @@
(state/set-sub-modal! installed-themes))
(rum/defc hook-ui-slot
([type payload] (hook-ui-slot type payload nil))
([type payload opts]
([type payload] (hook-ui-slot type payload nil #(plugin-handler/hook-plugin-app type % nil)))
([type payload opts callback]
(let [rs (util/rand-str 8)
id (str "slot__" rs)
*el-ref (rum/use-ref nil)]
(rum/use-effect!
(fn []
(let [timer (js/setTimeout
#(plugin-handler/hook-plugin-app type {:slot id :payload payload} nil)
100)]
(let [timer (js/setTimeout #(callback {:type type :slot id :payload payload}) 50)]
#(js/clearTimeout timer)))
[id])
@@ -962,6 +960,10 @@
:ref *el-ref
:on-mouse-down (fn [e] (util/stop e))})])))
(rum/defc hook-block-slot < rum/static
[type block]
(hook-ui-slot type {} nil #(plugin-handler/hook-plugin-block-slot block %)))
(rum/defc ui-item-renderer
[pid type {:keys [key template prefix]}]
(let [*el (rum/use-ref nil)

View File

@@ -559,10 +559,13 @@
display: flex;
align-items: center;
padding-top: 3px;
flex-wrap: wrap;
label {
padding-right: 15px;
user-select: none;
white-space: nowrap;
margin-top: 2px;
}
input {
@@ -895,6 +898,17 @@ html[data-theme='dark'] {
display: inline-flex;
}
}
.block-slotted-body {
.lsp-hook-ui-slot {
display: block;
[data-injected-ui] {
display: flex;
flex-direction: column;
}
}
}
}
.ui__modal[label=plugins-dashboard] {

View File

@@ -32,7 +32,7 @@
[:div
[:h1.title
"Input command"]
[:div.mt-4.mb-4.relative.rounded-md.shadow-sm.max-w-xs
[:div.mt-4.mb-4.relative.rounded-md.shadow-sm
[:input#run-command.form-input.font-mono.block.w-full.sm:text-sm.sm:leading-5
{:autoFocus true
:on-key-down util/stop-propagation

View File

@@ -115,46 +115,48 @@
id (:id highlight)
new? (nil? id)
content (:content highlight)
text? (string/blank? (:image content))
area? (not (string/blank? (:image content)))
action-fn! (fn [action clear?]
(when-let [action (and action (name action))]
(case action
"ref"
(pdf-assets/copy-hl-ref! highlight)
(let [highlight (if (fn? highlight) (highlight) highlight)
content (:content highlight)]
(case action
"ref"
(pdf-assets/copy-hl-ref! highlight)
"copy"
(do
(util/copy-to-clipboard!
(or (:text content) (pdf-utils/fix-selection-text-breakline (.toString selection))))
(pdf-utils/clear-all-selection))
"copy"
(do
(util/copy-to-clipboard!
(or (:text content) (pdf-utils/fix-selection-text-breakline (.toString selection))))
(pdf-utils/clear-all-selection))
"link"
(pdf-assets/goto-block-ref! highlight)
"link"
(pdf-assets/goto-block-ref! highlight)
"del"
(do
(del-hl! highlight)
(pdf-assets/del-ref-block! highlight)
(pdf-assets/unlink-hl-area-image$ viewer (:pdf/current @state/state) highlight))
"del"
(do
(del-hl! highlight)
(pdf-assets/del-ref-block! highlight)
(pdf-assets/unlink-hl-area-image$ viewer (:pdf/current @state/state) highlight))
"hook"
:dune
"hook"
:dune
;; colors
(let [properties {:color action}]
(if-not id
;; add highlight
(let [highlight (merge (if (fn? highlight) (highlight) highlight)
{:id (pdf-utils/gen-uuid)
:properties properties})]
(add-hl! highlight)
(pdf-utils/clear-all-selection)
(pdf-assets/copy-hl-ref! highlight))
;; colors
(let [properties {:color action}]
(if-not id
;; add highlight
(let [highlight (merge highlight
{:id (pdf-utils/gen-uuid)
:properties properties})]
(add-hl! highlight)
(pdf-utils/clear-all-selection)
(pdf-assets/copy-hl-ref! highlight))
;; update highlight
(upd-hl! (assoc highlight :properties properties)))
;; update highlight
(upd-hl! (assoc highlight :properties properties)))
(reset! *highlight-last-color (keyword action))))
(reset! *highlight-last-color (keyword action)))))
(and clear? (js/setTimeout #(clear-ctx-tip!) 68))))]
@@ -185,20 +187,20 @@
(and id [:li.item {:data-action "ref"} (t :pdf/copy-ref)])
(and (not (:image content)) [:li.item {:data-action "copy"} (t :pdf/copy-text)])
(and (not area?) [:li.item {:data-action "copy"} (t :pdf/copy-text)])
(and id [:li.item {:data-action "link"} (t :pdf/linked-ref)])
(and id [:li.item {:data-action "del"} (t :delete)])
(when (and config/lsp-enabled? text?)
(when (and config/lsp-enabled? (not area?))
(for [[_ {:keys [key label extras] :as _cmd} action pid]
(state/get-plugins-commands-with-type :highlight-context-menu-item)]
[:li.item {:key key
:data-action "hook"
:on-click #(do
:on-click #(let [highlight (if (fn? highlight) (highlight) highlight)]
(commands/exec-plugin-simple-command!
pid {:key key :content content :point point} action)
pid {:key key :content (:content highlight) :point point} action)
(when (true? (:clearSelection extras))
(pdf-utils/clear-all-selection)))}

View File

@@ -40,6 +40,7 @@
[frontend.handler.search :as search-handler]
[frontend.handler.ui :as ui-handler]
[frontend.handler.user :as user-handler]
[frontend.handler.shell :as shell-handler]
[frontend.handler.web.nfs :as nfs-handler]
[frontend.mobile.core :as mobile]
[frontend.mobile.util :as mobile-util]
@@ -918,6 +919,10 @@
[:p "Don't forget to re-index your graph when all the conflicts are resolved."]]
:status :error}]))
(defmethod handle :run/cli-command [[_ command content]]
(when (and command (not (string/blank? content)))
(shell-handler/run-cli-command-wrapper! command content)))
(defn run!
[]
(let [chan (state/get-events-chan)]

View File

@@ -24,11 +24,11 @@
[input]
(when input
(walk/postwalk
(fn [a]
(cond
(keyword? a) (csk/->camelCase (name a))
(uuid? a) (str a)
:else a)) input)))
(fn [a]
(cond
(keyword? a) (csk/->camelCase (name a))
(uuid? a) (str a)
:else a)) input)))
(defn invoke-exported-api
[type & args]
@@ -71,42 +71,42 @@
[refresh?]
(if (or refresh? (nil? (:plugin/marketplace-pkgs @state/state)))
(p/create
(fn [resolve reject]
(let [on-ok (fn [res]
(if-let [res (and res (bean/->clj res))]
(let [pkgs (:packages res)]
(state/set-state! :plugin/marketplace-pkgs pkgs)
(resolve pkgs))
(reject nil)))]
(if (state/http-proxy-enabled-or-val?)
(-> (ipc/ipc :httpFetchJSON plugins-url)
(p/then on-ok)
(p/catch reject))
(util/fetch plugins-url on-ok reject)))))
(fn [resolve reject]
(let [on-ok (fn [res]
(if-let [res (and res (bean/->clj res))]
(let [pkgs (:packages res)]
(state/set-state! :plugin/marketplace-pkgs pkgs)
(resolve pkgs))
(reject nil)))]
(if (state/http-proxy-enabled-or-val?)
(-> (ipc/ipc :httpFetchJSON plugins-url)
(p/then on-ok)
(p/catch reject))
(util/fetch plugins-url on-ok reject)))))
(p/resolved (:plugin/marketplace-pkgs @state/state))))
(defn load-marketplace-stats
[refresh?]
(if (or refresh? (nil? (:plugin/marketplace-stats @state/state)))
(p/create
(fn [resolve reject]
(let [on-ok (fn [^js res]
(if-let [res (and res (bean/->clj res))]
(do
(state/set-state!
:plugin/marketplace-stats
(into {} (map (fn [[k stat]]
[k (assoc stat
:total_downloads
(reduce (fn [a b] (+ a (get b 2))) 0 (:releases stat)))])
res)))
(resolve nil))
(reject nil)))]
(if (state/http-proxy-enabled-or-val?)
(-> (ipc/ipc :httpFetchJSON stats-url)
(p/then on-ok)
(p/catch reject))
(util/fetch stats-url on-ok reject)))))
(fn [resolve reject]
(let [on-ok (fn [^js res]
(if-let [res (and res (bean/->clj res))]
(do
(state/set-state!
:plugin/marketplace-stats
(into {} (map (fn [[k stat]]
[k (assoc stat
:total_downloads
(reduce (fn [a b] (+ a (get b 2))) 0 (:releases stat)))])
res)))
(resolve nil))
(reject nil)))]
(if (state/http-proxy-enabled-or-val?)
(-> (ipc/ipc :httpFetchJSON stats-url)
(p/then on-ok)
(p/catch reject))
(util/fetch stats-url on-ok reject)))))
(p/resolved nil)))
(defn check-or-update-marketplace-plugin
@@ -114,32 +114,46 @@
(when-not (and (:plugin/installing @state/state)
(not (plugin-common-handler/installed? id)))
(p/catch
(p/then
(do (state/set-state! :plugin/installing pkg)
(p/catch
(load-marketplace-plugins false)
(fn [^js e]
(state/reset-all-updates-state)
(throw e))))
(fn [mfts]
(p/then
(do (state/set-state! :plugin/installing pkg)
(p/catch
(load-marketplace-plugins false)
(fn [^js e]
(state/reset-all-updates-state)
(throw e))))
(fn [mfts]
(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))
(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 e)
(state/set-state! :plugin/installing nil)
(js/console.error e)))))
(fn [^js e]
(error-handler e)
(state/set-state! :plugin/installing nil)
(js/console.error e)))))
(defn get-plugin-inst
[id]
[pid]
(try
(js/LSPluginCore.ensurePlugin (name id))
(js/LSPluginCore.ensurePlugin (name pid))
(catch :default _e
nil)))
(defn call-plugin-user-model!
[pid key args]
(when-let [^js pl (get-plugin-inst pid)]
(let [^js caller (.-caller pl)]
(.apply (.-callUserModelAsync caller) caller (bean/->js (list* (name key) args))))))
(defn call-plugin-user-command!
[pid key args]
(when-let [commands (and key (seq (get (:plugin/simple-commands @state/state) (keyword pid))))]
(when-let [matched (medley/find-first #(= (:key (second %)) key) commands)]
(let [[_ cmd action pid] matched]
(state/pub-event!
[:exec-plugin-cmd {:type type :key key :pid pid :cmd (assoc cmd :args args) :action action}])))))
(defn open-updates-downloading
[]
(when (and (not (:plugin/updates-downloading? @state/state))
@@ -168,7 +182,7 @@
(defn setup-install-listener!
[]
(let [channel (name :lsp-installed)
(let [channel (name :lsp-installed)
listener (fn [^js _ ^js e]
(js/console.debug :lsp-installed e)
@@ -205,7 +219,7 @@
[(str (t :plugin/up-to-date) " :)") :success]
[error-code :error])
pending? (seq (:plugin/updates-pending @state/state))]
pending? (seq (:plugin/updates-pending @state/state))]
(if (and only-check pending?)
(state/consume-updates-coming-plugin payload false)
@@ -217,10 +231,10 @@
;; notify human tips
(notification/show!
(str
(if (= :error type) "[Error]" "")
(str "<" (:id payload) "> ")
msg) type)))
(str
(if (= :error type) "[Error]" "")
(str "<" (:id payload) "> ")
msg) type)))
(js/console.error payload))
@@ -272,18 +286,18 @@
(get keybinding-mode-handler-map (keyword mode)))
:action (fn []
(state/pub-event!
[:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}]))}]
[:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}]))}]
palette-cmd))
(defn simple-cmd-keybinding->shortcut-args
[pid key keybinding]
(let [id (keyword (str "plugin." pid "/" key))
(let [id (keyword (str "plugin." pid "/" key))
binding (:binding keybinding)
binding (if util/mac?
(or (:mac keybinding) binding)
binding)
mode (or (:mode keybinding) :global)
mode (get keybinding-mode-handler-map (keyword mode))]
mode (or (:mode keybinding) :global)
mode (get keybinding-mode-handler-map (keyword mode))]
[mode id {:binding binding}]))
(defn register-plugin-simple-command
@@ -320,7 +334,7 @@
(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}))
(fnil assoc {}) key (merge opts {:pid pid}))
true)))))
(defn unregister-plugin-resources
@@ -350,7 +364,7 @@
[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})
{:key key :edit edit :before before :subs subs :render render})
(swap! *fenced-code-providers conj pid)
#(swap! *fenced-code-providers disj pid)))
@@ -366,7 +380,7 @@
[pid type {:keys [enhancer] :as _opts}]
(when-let [key (and type (keyword type))]
(register-plugin-resources pid :extensions-enhancers
{:key key :enhancer enhancer})
{:key key :enhancer enhancer})
(swap! *extensions-enhancer-providers conj pid)
#(swap! *extensions-enhancer-providers disj pid)))
@@ -411,11 +425,11 @@
(when-not (string/blank? content)
(let [content (if-not (string/blank? url)
(string/replace
content #"!\[[^\]]*\]\((.*?)\s*(\"(?:.*[^\"])\")?\s*\)"
(fn [[matched link]]
(if (and link (not (string/starts-with? link "http")))
(string/replace matched link (util/node-path.join url link))
matched)))
content #"!\[[^\]]*\]\((.*?)\s*(\"(?:.*[^\"])\")?\s*\)"
(fn [[matched link]]
(if (and link (not (string/starts-with? link "http")))
(string/replace matched link (util/node-path.join url link))
matched)))
content)]
(format/to-html content :markdown (gp-mldoc/default-config :markdown))))
(catch :default e
@@ -429,9 +443,9 @@
;; local
(-> (p/let [content (invoke-exported-api "load_plugin_readme" url)
content (parse-user-md-content content item)]
(and (string/blank? (string/trim content)) (throw nil))
(state/set-state! :plugin/active-readme [content item])
(state/set-sub-modal! (fn [_] (display))))
(and (string/blank? (string/trim content)) (throw nil))
(state/set-state! :plugin/active-readme [content item])
(state/set-sub-modal! (fn [_] (display))))
(p/catch #(do (js/console.warn %)
(notification/show! "No README content." :warn))))
;; market
@@ -441,8 +455,8 @@
[]
(when util/electron?
(p/let [path (ipc/ipc "openDialog")]
(when-not (:plugin/selected-unpacked-pkg @state/state)
(state/set-state! :plugin/selected-unpacked-pkg path)))))
(when-not (:plugin/selected-unpacked-pkg @state/state)
(state/set-state! :plugin/selected-unpacked-pkg path)))))
(defn reset-unpacked-state
[]
@@ -482,6 +496,11 @@
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 hook-plugin-block-slot
[block payload]
(when-let [type (and block (str "slot:" (:block/uuid block)))]
(hook-plugin-editor type (merge payload block) nil)))
(defn get-ls-dotdir-root
[]
(ipc/ipc "getLogseqDotDirRoot"))
@@ -533,11 +552,11 @@
(defn- get-user-default-plugins
[]
(p/catch
(p/let [files ^js (ipc/ipc "getUserDefaultPlugins")
files (js->clj files)]
(map #(hash-map :url %) files))
(fn [e]
(js/console.error e))))
(p/let [files ^js (ipc/ipc "getUserDefaultPlugins")
files (js->clj files)]
(map #(hash-map :url %) files))
(fn [e]
(js/console.error e))))
(defn check-enabled-for-updates
[theme?]

View File

@@ -7,7 +7,8 @@
[promesa.core :as p]
[frontend.db :as db]
[frontend.state :as state]
[frontend.config :as config]))
[frontend.config :as config]
[frontend.util :as util]))
(defn run-git-command!
[command]
@@ -17,14 +18,15 @@
[command]
(ipc/ipc "runGitWithinCurrentGraph" command))
;; TODO: export to pdf/html/word
(defn run-pandoc-command!
[command]
(ipc/ipc "runPandoc" command))
(defn run-cli-command!
[command args]
(ipc/ipc :runCli {:command command
:args args
:returnResult true}))
(defn wrap-notification!
[command f args]
(p/let [result (f args)]
(p/let [result (f command args)]
(notification/show!
(if (string/blank? result)
[:p [:code.mr-1 (str command " " args) ]
@@ -33,22 +35,29 @@
:success
false)))
(def commands-denylist
#{"rm" "mv" "rename" "dd" ">" "command" "sudo"})
(defn run-command!
[command]
(let [[command args] (gp-util/split-first " " command)
command (and command (string/lower-case command))]
(when (and (not (string/blank? command)) (not (string/blank? args)))
(let [args (string/trim args)]
(case (keyword command)
:git
(wrap-notification! command run-git-command! args)
(let [[command args]
(if (and (string? command) (string/includes? command " "))
(gp-util/split-first " " command)
[command ""])
command (and command (string/lower-case command))
args (-> args str string/trim)]
(when-not (string/blank? command)
(cond
(contains? commands-denylist command)
(notification/show!
[:div (str command " is too dangerous!")]
:error)
;; :pandoc
;; (wrap-notification! command run-pandoc-command! args)
(= "git" command)
(wrap-notification! command (fn [_ args] (run-git-command! args)) args)
(notification/show!
[:div (str command " is not supported yet!")]
:error))))))
:else
(run-cli-command! command args)))))
;; git show $REV:$FILE
(defn- get-versioned-file-content
@@ -90,3 +99,11 @@
(notification/show!
[:div "git config successfully!"]
:success)))
(defn run-cli-command-wrapper!
[command content]
(let [args (case command
"alda" (util/format "play -c \"%s\"" content)
;; TODO: plugin slot
content)]
(run-cli-command! command args)))

View File

@@ -1,12 +1,16 @@
(ns frontend.modules.layout.core
(:require [cljs-bean.core :as bean]
[goog.object :as gobj]
[frontend.util :as util]))
(defonce *movable-containers (atom {}))
(defn- calc-layout-data
[^js cnt ^js _evt]
(.toJSON (.getBoundingClientRect cnt)))
(let [^js o (.toJSON (.getBoundingClientRect cnt))]
(set! (.-vw o) (gobj/get js/visualViewport "width"))
(set! (.-vh o) (gobj/get js/visualViewport "height"))
o))
(defn ^:export move-container-to-top
[identity]

View File

@@ -1437,7 +1437,7 @@ Similar to re-frame subscriptions"
[value]
(set-state! :network/online? value))
(defn get-plugins-commands
(defn get-plugins-slash-commands
[]
(mapcat seq (flatten (vals (:plugin/installed-slash-commands @state)))))
@@ -1542,6 +1542,12 @@ Similar to re-frame subscriptions"
(set-state! [:plugin/installed-hooks hook-or-all] (disj coll pid))))
true))
(defn slot-hook-exist?
[uuid]
(when-let [type (and uuid (string/replace (str uuid) "-" "_"))]
(when-let [hooks (sub :plugin/installed-hooks)]
(contains? hooks (str "hook:editor:slot_" type)))))
(defn active-tldraw-app
[]
(when-let [tldraw-el (.closest js/document.activeElement ".logseq-tldraw[data-tlapp]")]

View File

@@ -118,25 +118,25 @@
;; get app base info
[]
(bean/->js
(normalize-keyword-for-json
{:version fv/version})))
(normalize-keyword-for-json
{:version fv/version})))
(def ^:export get_user_configs
(fn []
(bean/->js
(normalize-keyword-for-json
{:preferred-language (:preferred-language @state/state)
:preferred-theme-mode (:ui/theme @state/state)
:preferred-format (state/get-preferred-format)
:preferred-workflow (state/get-preferred-workflow)
:preferred-todo (state/get-preferred-todo)
: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?)
:enabled-journals (state/enable-journals?)
:enabled-flashcards (state/enable-flashcards?)
:me (state/get-me)}))))
(normalize-keyword-for-json
{:preferred-language (:preferred-language @state/state)
:preferred-theme-mode (:ui/theme @state/state)
:preferred-format (state/get-preferred-format)
:preferred-workflow (state/get-preferred-workflow)
:preferred-todo (state/get-preferred-todo)
: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?)
:enabled-journals (state/enable-journals?)
:enabled-flashcards (state/enable-flashcards?)
:me (state/get-me)}))))
(def ^:export get_current_graph_configs
(fn []
@@ -188,7 +188,7 @@
path (util/node-path.join path "package.json")]
(fs/write-file! repo "" path (js/JSON.stringify data nil 2) {:skip-compare? true}))))
(def ^:export save_focused_code_editor
(def ^:export save_focused_code_editor_content
(fn []
(code-handler/save-code-editor!)))
@@ -277,7 +277,7 @@
(def ^:export read_plugin_storage_file
(fn [plugin-id file assets?]
(let [plugin-id (util/node-path.basename plugin-id)
sub-root (util/node-path.join "storages" plugin-id)]
sub-root (util/node-path.join "storages" plugin-id)]
(if (true? assets?)
(read_assetsdir_file file sub-root)
(read_dotdir_file file sub-root)))))
@@ -285,7 +285,7 @@
(def ^:export unlink_plugin_storage_file
(fn [plugin-id file assets?]
(let [plugin-id (util/node-path.basename plugin-id)
sub-root (util/node-path.join "storages" plugin-id)]
sub-root (util/node-path.join "storages" plugin-id)]
(if (true? assets?)
(unlink_assetsdir_file! file sub-root)
(unlink_dotdir_file! file sub-root)))))
@@ -327,7 +327,7 @@
(p/let [repo ""
path (plugin-handler/get-ls-dotdir-root)
path (util/node-path.join path "preferences.json")
_ (fs/create-if-not-exists repo "" path)
_ (fs/create-if-not-exists repo "" path)
json (fs/read-file "" path)
json (if (string/blank? json) "{}" json)]
(js/JSON.parse json))))
@@ -338,7 +338,7 @@
(p/let [repo ""
path (plugin-handler/get-ls-dotdir-root)
path (util/node-path.join path "preferences.json")]
(fs/write-file! repo "" path (js/JSON.stringify data nil 2) {:skip-compare? true})))))
(fs/write-file! repo "" path (js/JSON.stringify data nil 2) {:skip-compare? true})))))
(def ^:export load_plugin_user_settings
;; results [path data]
@@ -356,18 +356,18 @@
(fn [pid ^js cmd-actions]
(when-let [[cmd actions] (bean/->clj cmd-actions)]
(plugin-handler/register-plugin-slash-command
pid [cmd (mapv #(into [(keyword (first %))]
(rest %)) actions)]))))
pid [cmd (mapv #(into [(keyword (first %))]
(rest %)) actions)]))))
(def ^:export register_plugin_simple_command
(fn [pid ^js cmd-action palette?]
(when-let [[cmd action] (bean/->clj cmd-action)]
(let [action (assoc action 0 (keyword (first action)))
cmd (assoc cmd :key (string/replace (:key cmd) ":" "-"))
key (:key cmd)
keybinding (:keybinding cmd)
(let [action (assoc action 0 (keyword (first action)))
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))
action' #(state/pub-event! [:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action 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)
@@ -414,7 +414,7 @@
(fn [pid type ^js opts]
(when-let [opts (bean/->clj opts)]
(plugin-handler/register-plugin-ui-item
pid (assoc opts :type type)))))
pid (assoc opts :type type)))))
;; app
(def ^:export relaunch
@@ -465,16 +465,30 @@
(def ^:export push_state
(fn [^js k ^js params ^js query]
(rfe/push-state
(keyword k)
(bean/->clj params)
(bean/->clj query))))
(keyword k)
(bean/->clj params)
(bean/->clj query))))
(def ^:export replace_state
(fn [^js k ^js params ^js query]
(rfe/replace-state
(keyword k)
(bean/->clj params)
(bean/->clj query))))
(keyword k)
(bean/->clj params)
(bean/->clj query))))
(defn ^:export get_external_plugin
[pid]
(when-let [^js pl (plugin-handler/get-plugin-inst pid)]
(.toJSON pl)))
(defn ^:export invoke_external_plugin_cmd
[pid cmd-group cmd-key cmd-args]
(case (keyword cmd-group)
:models
(plugin-handler/call-plugin-user-model! pid cmd-key cmd-args)
:commands
(plugin-handler/call-plugin-user-command! pid cmd-key cmd-args)))
;; editor
(def ^:export check_editing
@@ -543,13 +557,13 @@
page
(let [properties (bean/->clj properties)
{:keys [redirect createFirstBlock format journal]} (bean/->clj opts)
name (page-handler/create!
name
{:redirect? (if (boolean? redirect) redirect true)
:journal? journal
:create-first-block? (if (boolean? createFirstBlock) createFirstBlock true)
:format format
:properties properties})]
name (page-handler/create!
name
{:redirect? (if (boolean? redirect) redirect true)
:journal? journal
:create-first-block? (if (boolean? createFirstBlock) createFirstBlock true)
:format format
:properties properties})]
(db-model/get-page name)))
(:db/id)
(db-utils/pull)
@@ -585,47 +599,47 @@
(def ^:export insert_block
(fn [block-uuid-or-page-name content ^js opts]
(let [{:keys [before sibling focus isPageBlock customUUID properties]} (bean/->clj opts)
page-name (and isPageBlock block-uuid-or-page-name)
custom-uuid (or customUUID (:id properties))
custom-uuid (when custom-uuid (uuid-or-throw-error custom-uuid))
edit-block? (if (nil? focus) true focus)
_ (when (and custom-uuid (db-model/query-block-by-uuid custom-uuid))
(throw (js/Error.
(util/format "Custom block UUID already exists (%s)." custom-uuid))))
block-uuid (if isPageBlock nil (uuid block-uuid-or-page-name))
block-uuid' (if (and (not sibling) before block-uuid)
(let [block (db/entity [:block/uuid block-uuid])
first-child (db-model/get-by-parent-&-left (db/get-db)
(:db/id block)
(:db/id block))]
(if first-child
(:block/uuid first-child)
block-uuid))
block-uuid)
page-name (and isPageBlock block-uuid-or-page-name)
custom-uuid (or customUUID (:id properties))
custom-uuid (when custom-uuid (uuid-or-throw-error custom-uuid))
edit-block? (if (nil? focus) true focus)
_ (when (and custom-uuid (db-model/query-block-by-uuid custom-uuid))
(throw (js/Error.
(util/format "Custom block UUID already exists (%s)." custom-uuid))))
block-uuid (if isPageBlock nil (uuid block-uuid-or-page-name))
block-uuid' (if (and (not sibling) before block-uuid)
(let [block (db/entity [:block/uuid block-uuid])
first-child (db-model/get-by-parent-&-left (db/get-db)
(:db/id block)
(:db/id block))]
(if first-child
(:block/uuid first-child)
block-uuid))
block-uuid)
insert-at-first-child? (not= block-uuid' block-uuid)
[sibling? before?] (if insert-at-first-child?
[true true]
[sibling before])
before? (if (and (false? sibling?) before? (not insert-at-first-child?))
false
before?)
new-block (editor-handler/api-insert-new-block!
content
{:block-uuid block-uuid'
:sibling? sibling?
:before? before?
:edit-block? edit-block?
:page page-name
:custom-uuid custom-uuid
:properties (merge properties
(when custom-uuid {:id custom-uuid}))})]
before? (if (and (false? sibling?) before? (not insert-at-first-child?))
false
before?)
new-block (editor-handler/api-insert-new-block!
content
{:block-uuid block-uuid'
:sibling? sibling?
:before? before?
:edit-block? edit-block?
:page page-name
:custom-uuid custom-uuid
:properties (merge properties
(when custom-uuid {:id custom-uuid}))})]
(bean/->js (normalize-keyword-for-json new-block)))))
(def ^:export insert_batch_block
(fn [block-uuid ^js batch-blocks ^js opts]
(when-let [block (db-model/query-block-by-uuid (uuid-or-throw-error block-uuid))]
(when-let [bb (bean/->clj batch-blocks)]
(let [bb (if-not (vector? bb) (vector bb) bb)
(let [bb (if-not (vector? bb) (vector bb) bb)
{:keys [sibling keepUUID]} (bean/->clj opts)
keep-uuid? (or keepUUID false)
_ (when keep-uuid? (doseq
@@ -641,16 +655,16 @@
(def ^:export remove_block
(fn [block-uuid ^js _opts]
(let [includeChildren true
repo (state/get-current-repo)]
repo (state/get-current-repo)]
(editor-handler/delete-block-aux!
{:block/uuid (uuid-or-throw-error block-uuid) :repo repo} includeChildren)
nil)))
(def ^:export update_block
(fn [block-uuid content ^js _opts]
(let [repo (state/get-current-repo)
(let [repo (state/get-current-repo)
edit-input (state/get-edit-input-id)
editing? (and edit-input (string/ends-with? edit-input (str block-uuid)))]
editing? (and edit-input (string/ends-with? edit-input (str block-uuid)))]
(if editing?
(state/set-edit-content! edit-input content)
(editor-handler/save-block! repo (uuid-or-throw-error block-uuid) content))
@@ -659,16 +673,16 @@
(def ^:export move_block
(fn [src-block-uuid target-block-uuid ^js opts]
(let [{:keys [before children]} (bean/->clj opts)
move-to (cond
(boolean before)
:top
move-to (cond
(boolean before)
:top
(boolean children)
:nested
(boolean children)
:nested
:else
nil)
src-block (db-model/query-block-by-uuid (uuid-or-throw-error src-block-uuid))
:else
nil)
src-block (db-model/query-block-by-uuid (uuid-or-throw-error src-block-uuid))
target-block (db-model/query-block-by-uuid (uuid-or-throw-error target-block-uuid))]
(editor-dnd-handler/move-blocks nil [src-block] target-block move-to) nil)))
@@ -680,15 +694,15 @@
(when-not (contains? block :block/name)
(when-let [uuid (:block/uuid block)]
(let [{:keys [includeChildren]} (bean/->clj opts)
repo (state/get-current-repo)
repo (state/get-current-repo)
block (if includeChildren
;; nested children results
(first (outliner-tree/blocks->vec-tree
(db-model/get-block-and-children repo uuid) uuid))
(db-model/get-block-and-children repo uuid) uuid))
;; attached shallow children
(assoc block :block/children
(map #(list :uuid (get-in % [:data :block/uuid]))
(db/get-block-immediate-children repo uuid))))]
(map #(list :uuid (get-in % [:data :block/uuid]))
(db/get-block-immediate-children repo uuid))))]
(bean/->js (normalize-keyword-for-json block))))))))
(def ^:export get_current_block
@@ -718,15 +732,15 @@
(fn [block-uuid ^js opts]
(let [block-uuid (uuid-or-throw-error block-uuid)]
(when-let [block (db-model/get-block-by-uuid block-uuid)]
(let [opts (bean/->clj opts)
opts (if (or (string? opts) (boolean? opts)) {:flag opts} opts)
{:keys [flag]} opts
flag (if (= "toggle" flag)
(not (util/collapsed? block))
(boolean flag))]
(if flag (editor-handler/collapse-block! block-uuid)
(editor-handler/expand-block! block-uuid))
nil)))))
(let [opts (bean/->clj opts)
opts (if (or (string? opts) (boolean? opts)) {:flag opts} opts)
{:keys [flag]} opts
flag (if (= "toggle" flag)
(not (util/collapsed? block))
(boolean flag))]
(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]
@@ -766,7 +780,7 @@
(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)
(let [page-name (:block/name page)
ref-blocks (if page-name
(db-model/get-page-referenced-blocks-full page-name)
(db-model/get-block-referenced-blocks (:block/uuid page)))
@@ -802,12 +816,12 @@
(defn ^:export prepend_block_in_page
[uuid-or-page-name content ^js opts]
(let [page? (not (util/uuid-string? uuid-or-page-name))
(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)}))]
_ (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')))
@@ -818,18 +832,18 @@
(defn ^:export append_block_in_page
[uuid-or-page-name content ^js opts]
(let [page? (not (util/uuid-string? uuid-or-page-name))
(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)}))]
_ (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}))
(when sibling? {:before false}))
src (if sibling? (str (:block/uuid block')) uuid-or-page-name)]
(insert_block src content (bean/->js opts))))))
@@ -855,13 +869,18 @@
[query & inputs]
(when-let [repo (state/get-current-repo)]
(when-let [db (db/get-db repo)]
(let [query (cljs.reader/read-string query)
(let [query (cljs.reader/read-string query)
resolved-inputs (map #(cond
(string? %)
(some-> % (cljs.reader/read-string) (query-react/resolve-input))
(fn? %)
(fn [& args]
(.apply % nil (clj->js (mapv bean/->js args))))
:else %)
inputs)
result (apply d/q query db resolved-inputs)]
result (apply d/q query db resolved-inputs)]
(bean/->js (normalize-keyword-for-json result false))))))
(defn ^:export custom_query
@@ -874,7 +893,7 @@
[]
(when-let [repo (state/get-current-repo)]
(when-let [db (db/get-db repo)]
(let [db-str (if db (db/db->string db) "")
(let [db-str (if db (db/db->string db) "")
data-str (str "data:text/edn;charset=utf-8," (js/encodeURIComponent db-str))]
(when-let [anchor (gdom/getElement "download")]
(.setAttribute anchor "href" data-str)
@@ -900,18 +919,18 @@
(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)
(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)))
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})])))
dir (config/get-repo-dir repo)
_ (fs/write-file! repo dir file content {:skip-compare? true})])))
;; ui
(defn ^:export show_msg
@@ -921,9 +940,9 @@
(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)]
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
@@ -939,7 +958,7 @@
(defn ^:export assets_list_files_of_current_graph
[^js exts]
(p/let [files (ipc/ipc :getAssetsFiles {:exts exts})]
(bean/->js files)))
(bean/->js files)))
;; experiments
(defn ^:export exper_load_scripts
@@ -947,28 +966,28 @@
(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})]]
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))))))
(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]))))
(keyword pid) type (reduce #(assoc %1 %2 (aget opts (name %2))) {}
[:edit :before :subs :render]))))
(defn ^:export exper_register_extensions_enhancer
[pid type enhancer]
(when-let [^js _pl (and (fn? enhancer) (plugin-handler/get-plugin-inst pid))]
(plugin-handler/register-extensions-enhancer
(keyword pid) type {:enhancer enhancer})))
(keyword pid) type {:enhancer enhancer})))
(defonce *request-k (volatile! 0))
@@ -1006,7 +1025,7 @@
(defn ^:export force_save_graph
[]
(p/let [_ (el/persist-dbs!)]
true))
true))
(def ^:export make_asset_url editor-handler/make-asset-url)

View File

@@ -1404,6 +1404,11 @@ combined-stream@^1.0.8:
dependencies:
delayed-stream "~1.0.0"
command-exists@1.2.9:
version "1.2.9"
resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==
commander@2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"