| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536 |
- import { SettingSchemaDesc, StyleString, UIOptions } from './LSPlugin'
- import { PluginLocal } from './LSPlugin.core'
- import * as nodePath from 'path'
- 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 {
- api: any
- apis: any
- }
- }
- 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://'
- 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'
- ))
- }
- export async function getSDKPathRoot(): Promise<string> {
- if (IS_DEV) {
- // TODO: cache in preference file
- return localStorage.getItem('LSP_DEV_SDK_ROOT') || 'http://localhost:8080'
- }
- const appPathRoot = await getAppPathRoot()
- return safetyPathJoin(appPathRoot, 'js')
- }
- export function isObject(item: any) {
- return item === Object(item) && !Array.isArray(item)
- }
- 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
- // after the decimal.
- return '_' + Math.random().toString(36).substr(2, 9)
- }
- export function ucFirst(str: string) {
- return str.charAt(0).toUpperCase() + str.slice(1)
- }
- export function withFileProtocol(path: string) {
- if (!path) return ''
- const reg = /^(http|file|lsp)/
- if (!reg.test(path)) {
- path = PROTOCOL_FILE + path
- }
- return path
- }
- export function safetyPathJoin(basePath: string, ...parts: Array<string>) {
- try {
- const url = new URL(basePath)
- if (!url.origin) throw new Error(null)
- const fullPath = path.join(basePath.substr(url.origin.length), ...parts)
- return url.origin + fullPath
- } catch (e) {
- return path.join(basePath, ...parts)
- }
- }
- export function safetyPathNormalize(basePath: string) {
- if (!basePath?.match(/^(http?|lsp|assets):/)) {
- basePath = path.normalize(basePath)
- }
- return basePath
- }
- /**
- * @param timeout milliseconds
- * @param tag string
- */
- export function deferred<T = any>(timeout?: number, tag?: string) {
- let resolve: any, reject: any
- let settled = false
- const timeFn = (r: Function) => {
- return (v: T) => {
- timeout && clearTimeout(timeout)
- r(v)
- settled = true
- }
- }
- const promise = new Promise<T>((resolve1, reject1) => {
- resolve = timeFn(resolve1)
- reject = timeFn(reject1)
- if (timeout) {
- // @ts-ignore
- timeout = setTimeout(
- () => reject(new Error(`[deferred timeout] ${tag}`)),
- timeout
- )
- }
- })
- return {
- created: Date.now(),
- 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 = safeSnakeCase(method)
- const logseqHostExportedApi = Object.assign(
- // @ts-ignore
- window.logseq?.api || {},
- callables
- )
- 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(this, args)
- }
- export function setupIframeSandbox(
- props: Record<string, any>,
- target: HTMLElement
- ) {
- const iframe = document.createElement('iframe')
- iframe.classList.add('lsp-iframe-sandbox')
- Object.entries(props).forEach(([k, v]) => {
- iframe.setAttribute(k, v)
- })
- target.appendChild(iframe)
- return async () => {
- target.removeChild(iframe)
- }
- }
- export function setupInjectedStyle(
- style: StyleString,
- attrs: Record<string, any>
- ) {
- const key = attrs['data-injected-style']
- let el = key && document.querySelector(`[data-injected-style=${key}]`)
- if (el) {
- el.textContent = style
- return
- }
- el = document.createElement('style')
- el.textContent = style
- attrs &&
- Object.entries(attrs).forEach(([k, v]) => {
- el.setAttribute(k, v)
- })
- document.head.append(el)
- return () => {
- document.head.removeChild(el)
- }
- }
- const injectedUIEffects = new Map<string, () => void>()
- // @ts-ignore
- window.__injectedUIEffects = injectedUIEffects
- export function setupInjectedUI(
- this: PluginLocal,
- ui: UIOptions,
- attrs: Record<string, string>,
- initialCallback?: (e: { el: HTMLElement; float: boolean }) => void
- ) {
- let slot: string = ''
- let selector: string
- let float: boolean
- const pl = this
- if ('slot' in ui) {
- slot = ui.slot
- selector = `#${slot}`
- } else if ('path' in ui) {
- selector = ui.path
- } else {
- float = true
- }
- const id = `${pl.id}--${ui.key || genID()}`
- const key = id
- const target = float
- ? document.body
- : selector && document.querySelector(selector)
- if (!target) {
- console.error(
- `${this.debugTag} can not resolve selector target ${selector}`
- )
- return
- }
- if (ui.template) {
- // safe template
- ui.template = DOMPurify.sanitize(ui.template, {
- ADD_TAGS: ['iframe'],
- ALLOW_UNKNOWN_PROTOCOLS: true,
- ADD_ATTR: [
- 'allow',
- 'src',
- 'allowfullscreen',
- 'frameborder',
- 'scrolling',
- 'target',
- ],
- })
- } else {
- // remove ui
- injectedUIEffects.get(id)?.call(null)
- return
- }
- let el = document.querySelector(`#${id}`) as HTMLElement
- let content = float ? el?.querySelector('.ls-ui-float-content') : el
- if (content) {
- content.innerHTML = ui.template
- // update attributes
- 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)
- ) {
- return
- }
- el.style[k] = v
- })
- return
- }
- el = document.createElement('div')
- el.id = id
- el.dataset.injectedUi = key || ''
- if (float) {
- content = document.createElement('div')
- content.classList.add('ls-ui-float-content')
- el.appendChild(content)
- } else {
- content = el
- }
- // TODO: enhance template
- content.innerHTML = ui.template
- attrs &&
- Object.entries(attrs).forEach(([k, v]) => {
- el.setAttribute(k, v)
- })
- ui.style &&
- 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')
- 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,
- }))
- }
- if (!!slot && ui.reset) {
- 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)
- // TODO: How handle events
- ;[
- 'click',
- 'focus',
- 'focusin',
- 'focusout',
- 'blur',
- 'dblclick',
- 'keyup',
- 'keypress',
- 'keydown',
- 'change',
- 'input',
- 'contextmenu'
- ].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 { preventDefault } = trigger.dataset
- const msgType = trigger.dataset[`on${ucFirst(type)}`]
- if (msgType) pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
- if (preventDefault?.toLowerCase() === 'true') e.preventDefault()
- },
- false
- )
- })
- // callback
- initialCallback?.({ el, float })
- teardownUI = () => {
- disposeFloat?.()
- injectedUIEffects.delete(id)
- target!.removeChild(el)
- }
- injectedUIEffects.set(id, teardownUI)
- return teardownUI
- }
- export function cleanInjectedUI(
- id: string
- ) {
- if (!injectedUIEffects.has(id)) return
- const clean = injectedUIEffects.get(id)
- try { clean() } catch (e) {
- console.warn('[CLEAN Injected UI] ', id, e)
- }
- }
- 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 = {}
- if (target) {
- obj.type = e.type
- const ds = target.dataset
- const FLAG_RECT = 'rect'
- ;['value', 'id', 'className', 'dataset', FLAG_RECT].forEach((k) => {
- let v: any
- switch (k) {
- case FLAG_RECT:
- if (!ds.hasOwnProperty(FLAG_RECT)) return
- v = target.getBoundingClientRect().toJSON()
- break
- default:
- v = target[k]
- }
- if (typeof v === 'object') {
- v = { ...v }
- }
- obj[k] = v
- })
- }
- return obj
- }
- export function injectTheme(url: string) {
- const link = document.createElement('link')
- link.rel = 'stylesheet'
- link.href = url
- document.head.appendChild(link)
- const ejectTheme = () => {
- try {
- document.head.removeChild(link)
- } catch (e) {
- console.error(e)
- }
- }
- return ejectTheme
- }
- export function mergeSettingsWithSchema(
- settings: Record<string, any>,
- schema: Array<SettingSchemaDesc>
- ) {
- const defaults = (schema || []).reduce((a, b) => {
- if ('default' in b) {
- a[b.key] = b.default
- }
- return a
- }, {})
- // shadow copy
- return Object.assign(defaults, settings)
- }
|