diff --git a/src/services/wiki/wikiOperations/common.ts b/src/services/wiki/wikiOperations/common.ts new file mode 100644 index 00000000..3371bf18 --- /dev/null +++ b/src/services/wiki/wikiOperations/common.ts @@ -0,0 +1,85 @@ +import { WikiChannel } from '@/constants/channels'; + +export const wikiOperations = { + [WikiChannel.setState]: (stateKey: string, content: string) => ` + $tw.wiki.addTiddler({ title: '$:/state/${stateKey}', text: \`${content}\` }); + `, + [WikiChannel.addTiddler]: (nonceReceived: number, title: string, text: string, extraMeta = '{}', optionsString = '{}') => { + const options = JSON.parse(optionsString) as { withDate?: boolean }; + return ` + const dateObject = {}; + ${ + options.withDate === true + ? ` + const existedTiddler = $tw.wiki.getTiddler(\`${title}\`); + let created = existedTiddler?.fields?.created; + const modified = $tw.utils.stringifyDate(new Date()); + if (!existedTiddler) { + created = $tw.utils.stringifyDate(new Date()); + } + dateObject.created = created; + dateObject.modified = modified; + ` + : '' + } + $tw.wiki.addTiddler({ title: \`${title}\`, text: \`${text}\`, ...${extraMeta}, ...dateObject }); + `; + }, + [WikiChannel.getTiddlerText]: (title: string) => ` + $tw.wiki.getTiddlerText(\`${title}\`); +`, + + [WikiChannel.runFilter]: (filter: string) => ` + $tw.wiki.compileFilter(\`${filter}\`)() +`, + + [WikiChannel.getTiddlersAsJson]: (filter: string) => ` + $tw.wiki.filterTiddlers(\`${filter}\`).map(title => { + const tiddler = $tw.wiki.getTiddler(title); + return tiddler?.fields; + }).filter(item => item !== undefined) +`, + + [WikiChannel.setTiddlerText]: (title: string, value: string) => ` + $tw.wiki.setText(\`${title}\`, 'text', undefined, \`${value}\`); +`, + + [WikiChannel.renderWikiText]: (content: string) => ` + $tw.wiki.renderText("text/html", "text/vnd.tiddlywiki", \`${content.replaceAll('`', '\\`')}\`); +`, + + [WikiChannel.syncProgress]: (message: string) => ` + $tw.wiki.addTiddler({ title: '$:/state/notification/${WikiChannel.syncProgress}', text: \`${message}\` }); + $tw.notifier.display('$:/state/notification/${WikiChannel.syncProgress}'); +`, + + [WikiChannel.generalNotification]: (message: string) => ` + $tw.wiki.addTiddler({ title: \`$:/state/notification/${WikiChannel.generalNotification}\`, text: \`${message}\` }); + $tw.notifier.display(\`$:/state/notification/${WikiChannel.generalNotification}\`); +`, + + [WikiChannel.openTiddler]: (tiddlerName: string) => ` + let trimmedTiddlerName = \`${tiddlerName.replaceAll('\n', '')}\`; + let currentHandlerWidget = $tw.rootWidget; + let handled = false; + while (currentHandlerWidget && !handled) { + const bubbled = currentHandlerWidget.dispatchEvent({ type: "tm-navigate", navigateTo: trimmedTiddlerName, param: trimmedTiddlerName }); + handled = !bubbled; + currentHandlerWidget = currentHandlerWidget.children?.[0]; + } +`, + + [WikiChannel.sendActionMessage]: (actionMessage: string) => ` + $tw.rootWidget.dispatchEvent({ type: \`${actionMessage}\` }); +`, + + [WikiChannel.deleteTiddler]: (title: string) => ` + $tw.wiki.deleteTiddler(\`${title}\`); +`, + + [WikiChannel.printTiddler]: (tiddlerName: string) => ` + var page = (${printer.printTiddler.toString()})(\`${tiddlerName}\`); + page?.print?.(); + page?.close?.(); +`, +}; diff --git a/src/services/wiki/wikiOperations/web.ts b/src/services/wiki/wikiOperations/web.ts new file mode 100644 index 00000000..1a2f262f --- /dev/null +++ b/src/services/wiki/wikiOperations/web.ts @@ -0,0 +1,35 @@ +import { WikiChannel } from '@/constants/channels'; +import { wikiOperations as common } from './common'; + +export const wikiOperations = { + ...common + [WikiChannel.syncProgress]: (message: string) => ` + $tw.wiki.addTiddler({ title: '$:/state/notification/${WikiChannel.syncProgress}', text: \`${message}\` }); + $tw.notifier.display('$:/state/notification/${WikiChannel.syncProgress}'); + `, + + [WikiChannel.generalNotification]: (message: string) => ` + $tw.wiki.addTiddler({ title: \`$:/state/notification/${WikiChannel.generalNotification}\`, text: \`${message}\` }); + $tw.notifier.display(\`$:/state/notification/${WikiChannel.generalNotification}\`); + `, + + [WikiChannel.openTiddler]: (tiddlerName: string) => ` + let trimmedTiddlerName = \`${tiddlerName.replaceAll('\n', '')}\`; + let currentHandlerWidget = $tw.rootWidget; + let handled = false; + while (currentHandlerWidget && !handled) { + const bubbled = currentHandlerWidget.dispatchEvent({ type: "tm-navigate", navigateTo: trimmedTiddlerName, param: trimmedTiddlerName }); + handled = !bubbled; + currentHandlerWidget = currentHandlerWidget.children?.[0]; + } + `, + + [WikiChannel.printTiddler]: async (tiddlerName: string) => { + const printer = await import('../../libs/printer'); + return ` + var page = (${printer.printTiddler.toString()})(\`${tiddlerName}\`); + page?.print?.(); + page?.close?.(); + `; + }, +}; diff --git a/src/services/wiki/wikiWorker/wikiOperation.ts b/src/services/wiki/wikiWorker/wikiOperation.ts new file mode 100644 index 00000000..c7c24489 --- /dev/null +++ b/src/services/wiki/wikiWorker/wikiOperation.ts @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/promise-function-async */ +/* eslint-disable no-new-func */ +/* eslint-disable @typescript-eslint/no-implied-eval */ +/** + * Run some wiki operations on server side, so it works even when the wiki browser view is not visible. + */ + +import type { ITiddlyWiki } from 'tiddlywiki'; +import { IWikiOperations } from '../wikiOperations'; + +export class IpcServerRoutes { + private wikiInstance!: ITiddlyWiki; + private readonly pendingIpcServerRoutesRequests: Array<(value: void | PromiseLike) => void> = []; + + setWikiInstance(wikiInstance: ITiddlyWiki) { + this.wikiInstance = wikiInstance; + this.pendingIpcServerRoutesRequests.forEach((resolve) => { + resolve(); + }); + } + + private async waitForIpcServerRoutesAvailable() { + if (this.wikiInstance !== undefined) { + return; + } + await new Promise((resolve) => { + this.pendingIpcServerRoutesRequests.push(resolve); + }); + } + + private executeTWJavaScriptWhenIdle(script: string): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + try { + const result = new Function(`const $tw = arguments[0];return ${script}`)(this.wikiInstance) as unknown; + resolve(result); + } catch (error) { + reject(error); + } + }, 1); + }); + } + + // ██████ ██████ ███████ ██████ █████ ████████ ██ ██████ ███ ██ ███████ + // ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ + // ██ ██ ██████ █████ ██████ ███████ ██ ██ ██ ██ ██ ██ ██ ███████ + // ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + // ██████ ██ ███████ ██ ██ ██ ██ ██ ██ ██████ ██ ████ ███████ + public wikiOperation( + operationType: OP, + ...arguments_: Parameters + ): undefined | ReturnType { + if (typeof wikiOperations[operationType] !== 'function') { + throw new TypeError(`${operationType} gets no useful handler`); + } + if (!Array.isArray(arguments_)) { + // TODO: better type handling here + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/restrict-template-expressions + throw new TypeError(`${(arguments_ as any) ?? ''} (${typeof arguments_}) is not a good argument array for ${operationType}`); + } + // @ts-expect-error A spread argument must either have a tuple type or be passed to a rest parameter.ts(2556) this maybe a bug of ts... try remove this comment after upgrade ts. And the result become void is weird too. + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression + return wikiOperations[operationType](...arguments_) as unknown as ReturnType; + } +} + +export const ipcServerRoutes: IpcServerRoutes = new IpcServerRoutes();