LSPlugin.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. import EventEmitter from 'eventemitter3'
  2. import * as CSS from 'csstype'
  3. import { LSPluginCaller } from './LSPlugin.caller'
  4. import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
  5. export type PluginLocalIdentity = string
  6. export type ThemeOptions = {
  7. name: string
  8. url: string
  9. description?: string
  10. mode?: 'dark' | 'light'
  11. [key: string]: any
  12. }
  13. export type StyleString = string;
  14. export type StyleOptions = {
  15. key?: string
  16. style: StyleString
  17. }
  18. export type UIContainerAttrs = {
  19. draggable: boolean
  20. resizable: boolean
  21. [key: string]: any
  22. }
  23. export type UIBaseOptions = {
  24. key?: string
  25. replace?: boolean
  26. template: string | null
  27. style?: CSS.Properties
  28. attrs?: Record<string, string>
  29. close?: 'outside' | string
  30. reset?: boolean // reset slot content or not
  31. }
  32. export type UIPathIdentity = {
  33. /**
  34. * DOM selector
  35. */
  36. path: string
  37. }
  38. export type UISlotIdentity = {
  39. /**
  40. * Slot key
  41. */
  42. slot: string
  43. }
  44. export type UISlotOptions = UIBaseOptions & UISlotIdentity
  45. export type UIPathOptions = UIBaseOptions & UIPathIdentity
  46. export type UIOptions = UIBaseOptions | UIPathOptions | UISlotOptions
  47. export interface LSPluginPkgConfig {
  48. id: PluginLocalIdentity
  49. main: string
  50. entry: string // alias of main
  51. title: string
  52. mode: 'shadow' | 'iframe'
  53. themes: Array<ThemeOptions>
  54. icon: string
  55. [key: string]: any
  56. }
  57. export interface LSPluginBaseInfo {
  58. id: string // should be unique
  59. mode: 'shadow' | 'iframe'
  60. settings: {
  61. disabled: boolean
  62. [key: string]: any
  63. },
  64. [key: string]: any
  65. }
  66. export type IHookEvent = {
  67. [key: string]: any
  68. }
  69. export type IUserOffHook = () => void
  70. export type IUserHook<E = any, R = IUserOffHook> = (callback: (e: IHookEvent & E) => void) => IUserOffHook
  71. export type IUserSlotHook<E = any> = (callback: (e: IHookEvent & UISlotIdentity & E) => void) => void
  72. export type EntityID = number
  73. export type BlockUUID = string
  74. export type BlockUUIDTuple = ['uuid', BlockUUID]
  75. export type IEntityID = { id: EntityID }
  76. export type IBatchBlock = { content: string, properties?: Record<string, any>, children?: Array<IBatchBlock> }
  77. export interface AppUserInfo {
  78. [key: string]: any
  79. }
  80. /**
  81. * User's app configurations
  82. */
  83. export interface AppUserConfigs {
  84. preferredThemeMode: 'dark' | 'light'
  85. preferredFormat: 'markdown' | 'org'
  86. preferredDateFormat: string
  87. preferredStartOfWeek: string
  88. preferredLanguage: string
  89. preferredWorkflow: string
  90. [key: string]: any
  91. }
  92. /**
  93. * In Logseq, a graph represents a repository of connected pages and blocks
  94. */
  95. export interface AppGraphInfo {
  96. name: string
  97. url: string
  98. path: string
  99. [key: string]: any
  100. }
  101. /**
  102. * Block - Logseq's fundamental data structure.
  103. */
  104. export interface BlockEntity {
  105. id: EntityID // db id
  106. uuid: BlockUUID
  107. left: IEntityID
  108. format: 'markdown' | 'org'
  109. parent: IEntityID
  110. unordered: boolean
  111. content: string
  112. page: IEntityID
  113. // optional fields in dummy page
  114. anchor?: string
  115. body?: any
  116. children?: Array<BlockEntity | BlockUUIDTuple>
  117. container?: string
  118. file?: IEntityID
  119. level?: number
  120. meta?: { timestamps: any, properties: any, startPos: number, endPos: number }
  121. title?: Array<any>
  122. [key: string]: any
  123. }
  124. /**
  125. * Page is just a block with some specific properties.
  126. */
  127. export interface PageEntity {
  128. id: EntityID
  129. uuid: BlockUUID
  130. name: string
  131. originalName: string
  132. 'journal?': boolean
  133. file?: IEntityID
  134. namespace?: IEntityID
  135. format?: 'markdown' | 'org'
  136. journalDay?: number
  137. }
  138. export type BlockIdentity = BlockUUID | Pick<BlockEntity, 'uuid'>
  139. export type BlockPageName = string
  140. export type PageIdentity = BlockPageName | BlockIdentity
  141. export type SlashCommandActionCmd =
  142. 'editor/input'
  143. | 'editor/hook'
  144. | 'editor/clear-current-slash'
  145. | 'editor/restore-saved-cursor'
  146. export type SlashCommandAction = [cmd: SlashCommandActionCmd, ...args: any]
  147. export type SimpleCommandCallback = (e: IHookEvent) => void
  148. export type BlockCommandCallback = (e: IHookEvent & { uuid: BlockUUID }) => Promise<void>
  149. export type BlockCursorPosition = { left: number, top: number, height: number, pos: number, rect: DOMRect }
  150. export type SimpleCommandKeybinding = {
  151. mode?: 'global' | 'non-editing' | 'editing',
  152. binding: string,
  153. mac?: string // special for Mac OS
  154. }
  155. export type SettingSchemaDesc = {
  156. key: string
  157. type: 'string' | 'number' | 'boolean' | 'enum' | 'object'
  158. default: string | number | boolean | Array<any> | object | null
  159. title: string
  160. description: string // support markdown
  161. inputAs?: 'color' | 'date' | 'datetime-local' | 'range'
  162. enumChoices?: Array<string>
  163. enumPicker?: 'select' | 'radio' | 'checkbox' // default: select
  164. }
  165. export type ExternalCommandType =
  166. 'logseq.command/run' |
  167. 'logseq.editor/cycle-todo' |
  168. 'logseq.editor/down' |
  169. 'logseq.editor/up' |
  170. 'logseq.editor/expand-block-children' |
  171. 'logseq.editor/collapse-block-children' |
  172. 'logseq.editor/open-file-in-default-app' |
  173. 'logseq.editor/open-file-in-directory' |
  174. 'logseq.editor/select-all-blocks' |
  175. 'logseq.editor/toggle-open-blocks' |
  176. 'logseq.editor/zoom-in' |
  177. 'logseq.editor/zoom-out' |
  178. 'logseq.editor/indent' |
  179. 'logseq.editor/outdent' |
  180. 'logseq.editor/copy' |
  181. 'logseq.editor/cut' |
  182. 'logseq.go/home' |
  183. 'logseq.go/journals' |
  184. 'logseq.go/keyboard-shortcuts' |
  185. 'logseq.go/next-journal' |
  186. 'logseq.go/prev-journal' |
  187. 'logseq.go/search' |
  188. 'logseq.go/search-in-page' |
  189. 'logseq.go/tomorrow' |
  190. 'logseq.go/backward' |
  191. 'logseq.go/forward' |
  192. 'logseq.search/re-index' |
  193. 'logseq.sidebar/clear' |
  194. 'logseq.sidebar/open-today-page' |
  195. 'logseq.ui/goto-plugins' |
  196. 'logseq.ui/select-theme-color' |
  197. 'logseq.ui/toggle-brackets' |
  198. 'logseq.ui/toggle-cards' |
  199. 'logseq.ui/toggle-contents' |
  200. 'logseq.ui/toggle-document-mode' |
  201. 'logseq.ui/toggle-help' |
  202. 'logseq.ui/toggle-left-sidebar' |
  203. 'logseq.ui/toggle-right-sidebar' |
  204. 'logseq.ui/toggle-settings' |
  205. 'logseq.ui/toggle-theme' |
  206. 'logseq.ui/toggle-wide-mode' |
  207. 'logseq.command-palette/toggle'
  208. /**
  209. * App level APIs
  210. */
  211. export interface IAppProxy {
  212. // base
  213. getUserInfo: () => Promise<AppUserInfo | null>
  214. getUserConfigs: () => Promise<AppUserConfigs>
  215. // commands
  216. registerCommand: (
  217. type: string,
  218. opts: {
  219. key: string,
  220. label: string,
  221. desc?: string,
  222. palette?: boolean,
  223. keybinding?: SimpleCommandKeybinding
  224. },
  225. action: SimpleCommandCallback) => void
  226. registerCommandPalette: (
  227. opts: {
  228. key: string,
  229. label: string,
  230. keybinding?: SimpleCommandKeybinding
  231. },
  232. action: SimpleCommandCallback) => void
  233. invokeExternalCommand: (
  234. type: ExternalCommandType,
  235. ...args: Array<any>) => Promise<void>
  236. /**
  237. * Get state from app store
  238. * valid state is here
  239. * https://github.com/logseq/logseq/blob/master/src/main/frontend/state.cljs#L27
  240. *
  241. * @example
  242. * ```ts
  243. * const isDocMode = await logseq.App.getStateFromStore('document/mode?')
  244. * ```
  245. * @param path
  246. */
  247. getStateFromStore:
  248. <T = any>(path: string | Array<string>) => Promise<T>
  249. // native
  250. relaunch: () => Promise<void>
  251. quit: () => Promise<void>
  252. openExternalLink: (url: string) => Promise<void>
  253. /**
  254. * @link https://github.com/desktop/dugite/blob/master/docs/api/exec.md
  255. * @param args
  256. */
  257. execGitCommand: (args: string[]) => Promise<string>
  258. // graph
  259. getCurrentGraph: () => Promise<AppGraphInfo | null>
  260. // router
  261. pushState: (k: string, params?: Record<string, any>, query?: Record<string, any>) => void
  262. replaceState: (k: string, params?: Record<string, any>, query?: Record<string, any>) => void
  263. // ui
  264. queryElementById: (id: string) => Promise<string | boolean>
  265. showMsg: (content: string, status?: 'success' | 'warning' | 'error' | string) => void
  266. setZoomFactor: (factor: number) => void
  267. setFullScreen: (flag: boolean | 'toggle') => void
  268. setLeftSidebarVisible: (flag: boolean | 'toggle') => void
  269. setRightSidebarVisible: (flag: boolean | 'toggle') => void
  270. registerUIItem: (
  271. type: 'toolbar' | 'pagebar',
  272. opts: { key: string, template: string }
  273. ) => void
  274. registerPageMenuItem: (
  275. tag: string,
  276. action: (e: IHookEvent & { page: string }) => void
  277. ) => void
  278. // hook events
  279. onCurrentGraphChanged: IUserHook
  280. onThemeModeChanged: IUserHook<{ mode: 'dark' | 'light' }>
  281. onBlockRendererSlotted: IUserSlotHook<{ uuid: BlockUUID }>
  282. /**
  283. * provide ui slot to block `renderer` macro for `{{renderer arg1, arg2}}`
  284. *
  285. * @example
  286. * ```ts
  287. * // e.g. {{renderer :h1, hello world, green}}
  288. *
  289. * logseq.App.onMacroRendererSlotted(({ slot, payload: { arguments } }) => {
  290. * let [type, text, color] = arguments
  291. * if (type !== ':h1') return
  292. * logseq.provideUI({
  293. * key: 'h1-playground',
  294. * slot, template: `
  295. * <h2 style="color: ${color || 'red'}">${text}</h2>
  296. * `,
  297. * })
  298. * })
  299. * ```
  300. */
  301. onMacroRendererSlotted: IUserSlotHook<{ payload: { arguments: Array<string>, uuid: string, [key: string]: any } }>
  302. onPageHeadActionsSlotted: IUserSlotHook
  303. onRouteChanged: IUserHook<{ path: string, template: string }>
  304. onSidebarVisibleChanged: IUserHook<{ visible: boolean }>
  305. }
  306. /**
  307. * Editor related APIs
  308. */
  309. export interface IEditorProxy extends Record<string, any> {
  310. /**
  311. * register a custom command which will be added to the Logseq slash command list
  312. *
  313. * @param tag - displayed name of command
  314. * @param action - can be a single callback function to run when the command is called, or an array of fixed commands with arguments
  315. *
  316. * @example
  317. * ```ts
  318. * logseq.Editor.registerSlashCommand("Say Hi", () => {
  319. * console.log('Hi!')
  320. * })
  321. * ```
  322. *
  323. * @example
  324. * ```ts
  325. * logseq.Editor.registerSlashCommand("💥 Big Bang", [
  326. * ["editor/hook", "customCallback"],
  327. * ["editor/clear-current-slash"],
  328. * ]);
  329. * ```
  330. */
  331. registerSlashCommand: (
  332. tag: string,
  333. action: BlockCommandCallback | Array<SlashCommandAction>
  334. ) => unknown
  335. /**
  336. * register a custom command in the block context menu (triggered by right clicking the block dot)
  337. * @param tag - displayed name of command
  338. * @param action - can be a single callback function to run when the command is called
  339. */
  340. registerBlockContextMenuItem: (
  341. tag: string,
  342. action: BlockCommandCallback
  343. ) => unknown
  344. // block related APIs
  345. checkEditing: () => Promise<BlockUUID | boolean>
  346. /**
  347. * insert a string at the current cursor
  348. */
  349. insertAtEditingCursor: (content: string) => Promise<void>
  350. restoreEditingCursor: () => Promise<void>
  351. exitEditingMode: (selectBlock?: boolean) => Promise<void>
  352. getEditingCursorPosition: () => Promise<BlockCursorPosition | null>
  353. getEditingBlockContent: () => Promise<string>
  354. getCurrentPage: () => Promise<PageEntity | BlockEntity | null>
  355. getCurrentBlock: () => Promise<BlockEntity | null>
  356. getSelectedBlocks: () => Promise<Array<BlockEntity> | null>
  357. /**
  358. * get all blocks of the current page as a tree structure
  359. *
  360. * @example
  361. * ```ts
  362. * const blocks = await logseq.Editor.getCurrentPageBlocksTree()
  363. * initMindMap(blocks)
  364. * ```
  365. */
  366. getCurrentPageBlocksTree: () => Promise<Array<BlockEntity>>
  367. /**
  368. * get all blocks for the specified page
  369. *
  370. * @param srcPage - the page name or uuid
  371. */
  372. getPageBlocksTree: (srcPage: PageIdentity) => Promise<Array<BlockEntity>>
  373. insertBlock: (
  374. srcBlock: BlockIdentity,
  375. content: string,
  376. opts?: Partial<{ before: boolean; sibling: boolean; isPageBlock: boolean; properties: {} }>
  377. ) => Promise<BlockEntity | null>
  378. insertBatchBlock: (
  379. srcBlock: BlockIdentity,
  380. batch: IBatchBlock | Array<IBatchBlock>,
  381. opts?: Partial<{ before: boolean, sibling: boolean }>
  382. ) => Promise<Array<BlockEntity> | null>
  383. updateBlock: (
  384. srcBlock: BlockIdentity,
  385. content: string,
  386. opts?: Partial<{ properties: {} }>
  387. ) => Promise<void>
  388. removeBlock: (
  389. srcBlock: BlockIdentity
  390. ) => Promise<void>
  391. getBlock: (
  392. srcBlock: BlockIdentity | EntityID,
  393. opts?: Partial<{ includeChildren: boolean }>
  394. ) => Promise<BlockEntity | null>
  395. setBlockCollapsed: (
  396. uuid: BlockUUID,
  397. opts?: { flag: boolean | 'toggle' }
  398. ) => Promise<void>
  399. getPage: (
  400. srcPage: PageIdentity | EntityID,
  401. opts?: Partial<{ includeChildren: boolean }>
  402. ) => Promise<PageEntity | null>
  403. createPage: (
  404. pageName: BlockPageName,
  405. properties?: {},
  406. opts?: Partial<{ redirect: boolean, createFirstBlock: boolean, format: BlockEntity['format'], journal: boolean }>
  407. ) => Promise<PageEntity | null>
  408. deletePage: (
  409. pageName: BlockPageName
  410. ) => Promise<void>
  411. renamePage: (oldName: string, newName: string) => Promise<void>
  412. getAllPages: (repo?: string) => Promise<any>
  413. getPreviousSiblingBlock: (
  414. srcBlock: BlockIdentity
  415. ) => Promise<BlockEntity | null>
  416. getNextSiblingBlock: (srcBlock: BlockIdentity) => Promise<BlockEntity | null>
  417. moveBlock: (
  418. srcBlock: BlockIdentity,
  419. targetBlock: BlockIdentity,
  420. opts?: Partial<{ before: boolean; children: boolean }>
  421. ) => Promise<void>
  422. editBlock: (srcBlock: BlockIdentity, opts?: { pos: number }) => Promise<void>
  423. upsertBlockProperty: (
  424. block: BlockIdentity,
  425. key: string,
  426. value: any
  427. ) => Promise<void>
  428. removeBlockProperty: (block: BlockIdentity, key: string) => Promise<void>
  429. getBlockProperty: (block: BlockIdentity, key: string) => Promise<any>
  430. getBlockProperties: (block: BlockIdentity) => Promise<any>
  431. scrollToBlockInPage: (
  432. pageName: BlockPageName,
  433. blockId: BlockIdentity
  434. ) => void
  435. openInRightSidebar: (uuid: BlockUUID) => void
  436. // events
  437. onInputSelectionEnd: IUserHook<{ caret: any, point: { x: number, y: number }, start: number, end: number, text: string }>
  438. }
  439. /**
  440. * Datascript related APIs
  441. */
  442. export interface IDBProxy {
  443. /**
  444. * Run a DSL query
  445. * @link https://docs.logseq.com/#/page/queries
  446. * @param dsl
  447. */
  448. q: <T = any>(dsl: string) => Promise<Array<T> | null>
  449. /**
  450. * Run a datascript query
  451. */
  452. datascriptQuery: <T = any>(query: string) => Promise<T>
  453. }
  454. export interface ILSPluginThemeManager extends EventEmitter {
  455. themes: Map<PluginLocalIdentity, Array<ThemeOptions>>
  456. registerTheme (id: PluginLocalIdentity, opt: ThemeOptions): Promise<void>
  457. unregisterTheme (id: PluginLocalIdentity): Promise<void>
  458. selectTheme (opt?: ThemeOptions): Promise<void>
  459. }
  460. export type LSPluginUserEvents = 'ui:visible:changed' | 'settings:changed'
  461. export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
  462. /**
  463. * Connection status with the main app
  464. */
  465. connected: boolean
  466. /**
  467. * Duplex message caller
  468. */
  469. caller: LSPluginCaller
  470. /**
  471. * The plugin configurations from package.json
  472. */
  473. baseInfo: LSPluginBaseInfo
  474. /**
  475. * The plugin user settings
  476. */
  477. settings?: LSPluginBaseInfo['settings']
  478. /**
  479. * The main Logseq app is ready to run the plugin
  480. *
  481. * @param model - same as the model in `provideModel`
  482. */
  483. ready (model?: Record<string, any>): Promise<any>
  484. /**
  485. * @param callback - a function to run when the main Logseq app is ready
  486. */
  487. ready (callback?: (e: any) => void | {}): Promise<any>
  488. ready (
  489. model?: Record<string, any>,
  490. callback?: (e: any) => void | {}
  491. ): Promise<any>
  492. beforeunload: (callback: () => Promise<void>) => void
  493. /**
  494. * Create a object to hold the methods referenced in `provideUI`
  495. *
  496. * @example
  497. * ```ts
  498. * logseq.provideModel({
  499. * openCalendar () {
  500. * console.log('Open the calendar!')
  501. * }
  502. * })
  503. * ```
  504. */
  505. provideModel (model: Record<string, any>): this
  506. /**
  507. * Set the theme for the main Logseq app
  508. */
  509. provideTheme (theme: ThemeOptions): this
  510. /**
  511. * Inject custom css for the main Logseq app
  512. *
  513. * @example
  514. * ```ts
  515. * logseq.provideStyle(`
  516. * @import url("https://at.alicdn.com/t/font_2409735_r7em724douf.css");
  517. * )
  518. * ```
  519. *
  520. * @example
  521. * ```ts
  522. *
  523. * ```
  524. */
  525. provideStyle (style: StyleString | StyleOptions): this
  526. /**
  527. * Inject custom UI at specific DOM node.
  528. * Event handlers can not be passed by string, so you need to create them in `provideModel`
  529. *
  530. * @example
  531. * ```ts
  532. * logseq.provideUI({
  533. * key: 'open-calendar',
  534. * path: '#search',
  535. * template: `
  536. * <a data-on-click="openCalendar" onclick="alert('abc')' style="opacity: .6; display: inline-flex; padding-left: 3px;'>
  537. * <i class="iconfont icon-Calendaralt2"></i>
  538. * </a>
  539. * `
  540. * })
  541. * ```
  542. */
  543. provideUI (ui: UIOptions): this
  544. useSettingsSchema (schemas: Array<SettingSchemaDesc>): this
  545. updateSettings (attrs: Record<string, any>): void
  546. onSettingsChanged<T = any> (cb: (a: T, b: T) => void): IUserOffHook
  547. showSettingsUI (): void
  548. hideSettingsUI (): void
  549. setMainUIAttrs (attrs: Record<string, any>): void
  550. /**
  551. * Set the style for the plugin's UI
  552. *
  553. * @example
  554. * ```ts
  555. * logseq.setMainUIInlineStyle({
  556. * position: 'fixed',
  557. * zIndex: 11,
  558. * })
  559. * ```
  560. */
  561. setMainUIInlineStyle (style: CSS.Properties): void
  562. /**
  563. * show the plugin's UI
  564. */
  565. showMainUI (opts?: { autoFocus: boolean }): void
  566. /**
  567. * hide the plugin's UI
  568. */
  569. hideMainUI (opts?: { restoreEditingCursor: boolean }): void
  570. /**
  571. * toggle the plugin's UI
  572. */
  573. toggleMainUI (): void
  574. isMainUIVisible: boolean
  575. resolveResourceFullUrl (filePath: string): string
  576. App: IAppProxy & Record<string, any>
  577. Editor: IEditorProxy & Record<string, any>
  578. DB: IDBProxy
  579. FileStorage: LSPluginFileStorage
  580. }