From c87d7b49a1bdcebad80c806ba0f6c5b2c64f9b97 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Sat, 21 Sep 2024 13:58:52 +0200 Subject: [PATCH] implement #414 --- packages/core/src/config/ButtonConfig.ts | 8 +++ .../src/fields/button/ButtonActionRunner.ts | 53 +++++++++---------- .../core/src/fields/button/ButtonField.ts | 25 +++++---- packages/core/src/utils/Utils.ts | 8 ++- tests/fields/Button.test.ts | 36 ++++++++----- 5 files changed, 80 insertions(+), 50 deletions(-) diff --git a/packages/core/src/config/ButtonConfig.ts b/packages/core/src/config/ButtonConfig.ts index 2fd0654..4a77431 100644 --- a/packages/core/src/config/ButtonConfig.ts +++ b/packages/core/src/config/ButtonConfig.ts @@ -1,3 +1,5 @@ +import type { LinePosition } from 'packages/core/src/config/APIConfigs'; + export enum ButtonStyleType { DEFAULT = 'default', PRIMARY = 'primary', @@ -129,3 +131,9 @@ export interface ButtonConfig { action?: ButtonAction; actions?: ButtonAction[]; } + +export interface ButtonContext { + position: LinePosition | undefined; + isInGroup: boolean; + isInline: boolean; +} diff --git a/packages/core/src/fields/button/ButtonActionRunner.ts b/packages/core/src/fields/button/ButtonActionRunner.ts index 011654c..c626c6a 100644 --- a/packages/core/src/fields/button/ButtonActionRunner.ts +++ b/packages/core/src/fields/button/ButtonActionRunner.ts @@ -1,7 +1,7 @@ -import type { NotePosition } from 'packages/core/src/config/APIConfigs'; import type { ButtonAction, ButtonConfig, + ButtonContext, CommandButtonAction, CreateNoteButtonAction, InlineJsButtonAction, @@ -66,18 +66,13 @@ export class ButtonActionRunner { * @param inline whether the button is inline * @param position the position of the button in the note */ - async runButtonAction( - config: ButtonConfig, - filePath: string, - inline: boolean, - position: NotePosition | undefined, - ): Promise { + async runButtonActions(config: ButtonConfig, filePath: string, context: ButtonContext): Promise { try { if (config.action) { - await this.plugin.api.buttonActionRunner.runAction(config, config.action, filePath, inline, position); + await this.plugin.api.buttonActionRunner.runAction(config, config.action, filePath, context); } else if (config.actions) { for (const action of config.actions) { - await this.plugin.api.buttonActionRunner.runAction(config, action, filePath, inline, position); + await this.plugin.api.buttonActionRunner.runAction(config, action, filePath, context); } } else { console.warn('meta-bind | ButtonMDRC >> no action defined'); @@ -178,14 +173,13 @@ export class ButtonActionRunner { config: ButtonConfig | undefined, action: ButtonAction, filePath: string, - inline: boolean, - position: NotePosition | undefined, + buttonContext: ButtonContext, ): Promise { if (action.type === ButtonActionType.COMMAND) { await this.runCommandAction(action); return; } else if (action.type === ButtonActionType.JS) { - await this.runJSAction(config, action, filePath); + await this.runJSAction(config, action, filePath, buttonContext); return; } else if (action.type === ButtonActionType.OPEN) { await this.runOpenAction(action, filePath); @@ -209,16 +203,16 @@ export class ButtonActionRunner { await this.runReplaceInNoteAction(action, filePath); return; } else if (action.type === ButtonActionType.REPLACE_SELF) { - await this.runReplaceSelfAction(action, filePath, inline, position); + await this.runReplaceSelfAction(action, filePath, buttonContext); return; } else if (action.type === ButtonActionType.REGEXP_REPLACE_IN_NOTE) { - await this.runRegexpReplaceInNotAction(action, filePath); + await this.runRegexpReplaceInNoteAction(action, filePath); return; } else if (action.type === ButtonActionType.INSERT_INTO_NOTE) { await this.runInsertIntoNoteAction(action, filePath); return; } else if (action.type === ButtonActionType.INLINE_JS) { - await this.runInlineJsAction(config, action, filePath); + await this.runInlineJsAction(config, action, filePath, buttonContext); return; } @@ -231,7 +225,12 @@ export class ButtonActionRunner { this.plugin.internal.executeCommandById(action.command); } - async runJSAction(config: ButtonConfig | undefined, action: JSButtonAction, filePath: string): Promise { + async runJSAction( + config: ButtonConfig | undefined, + action: JSButtonAction, + filePath: string, + buttonContext: ButtonContext, + ): Promise { if (!this.plugin.settings.enableJs) { throw new MetaBindJsError({ errorLevel: ErrorLevel.CRITICAL, @@ -243,6 +242,7 @@ export class ButtonActionRunner { const configOverrides: Record = { buttonConfig: structuredClone(config), args: structuredClone(action.args), + buttonContext: structuredClone(buttonContext), }; const unloadCallback = await this.plugin.internal.jsEngineRunFile(action.file, filePath, configOverrides); unloadCallback(); @@ -334,20 +334,17 @@ export class ButtonActionRunner { async runReplaceSelfAction( action: ReplaceSelfButtonAction, filePath: string, - inline: boolean, - position: NotePosition | undefined, + buttonContext: ButtonContext, ): Promise { - if (inline) { + if (buttonContext.isInline) { throw new Error('Replace self action not supported for inline buttons'); } - const linePosition = position?.getPosition(); - - if (linePosition === undefined) { + if (buttonContext.position === undefined) { throw new Error('Position of the button in the note is unknown'); } - if (linePosition.lineStart > linePosition.lineEnd) { + if (buttonContext.position.lineStart > buttonContext.position.lineEnd) { throw new Error('Position of the button in the note is invalid'); } @@ -355,7 +352,7 @@ export class ButtonActionRunner { let splitContent = content.split('\n'); - if (linePosition.lineStart < 0 || linePosition.lineEnd > splitContent.length + 1) { + if (buttonContext.position.lineStart < 0 || buttonContext.position.lineEnd > splitContent.length + 1) { throw new Error('Position of the button in the note is out of bounds'); } @@ -364,15 +361,15 @@ export class ButtonActionRunner { : action.replacement; splitContent = [ - ...splitContent.slice(0, linePosition.lineStart), + ...splitContent.slice(0, buttonContext.position.lineStart), replacement, - ...splitContent.slice(linePosition.lineEnd + 1), + ...splitContent.slice(buttonContext.position.lineEnd + 1), ]; await this.plugin.internal.writeFilePath(filePath, splitContent.join('\n')); } - async runRegexpReplaceInNotAction(action: RegexpReplaceInNoteButtonAction, filePath: string): Promise { + async runRegexpReplaceInNoteAction(action: RegexpReplaceInNoteButtonAction, filePath: string): Promise { if (action.regexp === '') { throw new Error('Regexp cannot be empty'); } @@ -410,6 +407,7 @@ export class ButtonActionRunner { config: ButtonConfig | undefined, action: InlineJsButtonAction, filePath: string, + buttonContext: ButtonContext, ): Promise { if (!this.plugin.settings.enableJs) { throw new MetaBindJsError({ @@ -421,6 +419,7 @@ export class ButtonActionRunner { const configOverrides: Record = { buttonConfig: structuredClone(config), + buttonContext: structuredClone(buttonContext), }; const unloadCallback = await this.plugin.internal.jsEngineRunCode(action.code, filePath, configOverrides); unloadCallback(); diff --git a/packages/core/src/fields/button/ButtonField.ts b/packages/core/src/fields/button/ButtonField.ts index 0febbf9..0589c55 100644 --- a/packages/core/src/fields/button/ButtonField.ts +++ b/packages/core/src/fields/button/ButtonField.ts @@ -1,6 +1,6 @@ import type { NotePosition } from 'packages/core/src/config/APIConfigs'; import { RenderChildType } from 'packages/core/src/config/APIConfigs'; -import type { ButtonConfig } from 'packages/core/src/config/ButtonConfig'; +import type { ButtonConfig, ButtonContext } from 'packages/core/src/config/ButtonConfig'; import type { IPlugin } from 'packages/core/src/IPlugin'; import ButtonComponent from 'packages/core/src/utils/components/ButtonComponent.svelte'; import { Mountable } from 'packages/core/src/utils/Mountable'; @@ -12,7 +12,7 @@ export class ButtonField extends Mountable { plugin: IPlugin; config: ButtonConfig; filePath: string; - inline: boolean; + isInline: boolean; position: NotePosition | undefined; buttonComponent?: ReturnType; isInGroup: boolean; @@ -32,7 +32,7 @@ export class ButtonField extends Mountable { this.plugin = plugin; this.config = config; this.filePath = filePath; - this.inline = renderChildType === RenderChildType.INLINE; + this.isInline = renderChildType === RenderChildType.INLINE; this.position = position; this.isInGroup = isInGroup; this.isPreview = isPreview; @@ -41,9 +41,9 @@ export class ButtonField extends Mountable { protected onMount(targetEl: HTMLElement): void { DomHelpers.empty(targetEl); DomHelpers.removeAllClasses(targetEl); - DomHelpers.addClasses(targetEl, ['mb-button', this.inline ? 'mb-button-inline' : 'mb-button-block']); + DomHelpers.addClasses(targetEl, ['mb-button', this.isInline ? 'mb-button-inline' : 'mb-button-block']); - if (!this.inline && !this.isPreview && !this.isInGroup) { + if (!this.isInline && !this.isPreview && !this.isInGroup) { if (this.config.id) { this.plugin.api.buttonManager.addButton(this.filePath, this.config); } @@ -68,23 +68,30 @@ export class ButtonField extends Mountable { label: this.config.label, tooltip: isTruthy(this.config.tooltip) ? this.config.tooltip : this.config.label, onclick: async (): Promise => { - await this.plugin.api.buttonActionRunner.runButtonAction( + await this.plugin.api.buttonActionRunner.runButtonActions( this.config, this.filePath, - this.inline, - this.position, + this.getContext(), ); }, }, }); } + public getContext(): ButtonContext { + return { + position: this.position?.getPosition(), + isInGroup: this.isInGroup, + isInline: this.isInline, + }; + } + protected onUnmount(): void { if (this.buttonComponent) { unmount(this.buttonComponent); } - if (!this.inline && !this.isPreview) { + if (!this.isInline && !this.isPreview) { if (this.config?.id) { this.plugin.api.buttonManager.removeButton(this.filePath, this.config.id); } diff --git a/packages/core/src/utils/Utils.ts b/packages/core/src/utils/Utils.ts index 9e5005b..cf39dc1 100644 --- a/packages/core/src/utils/Utils.ts +++ b/packages/core/src/utils/Utils.ts @@ -138,7 +138,13 @@ export function isFalsy(value: unknown): boolean { return !value; } -export function deepFreeze(object: T): Readonly { +export function deepFreeze(object: T): Readonly; +export function deepFreeze(object: T | undefined): Readonly; +export function deepFreeze(object: T | undefined): Readonly { + if (object === undefined) { + return object; + } + // Retrieve the property names defined on object const propNames: (string | symbol)[] = Reflect.ownKeys(object); diff --git a/tests/fields/Button.test.ts b/tests/fields/Button.test.ts index 474d2eb..672886d 100644 --- a/tests/fields/Button.test.ts +++ b/tests/fields/Button.test.ts @@ -8,7 +8,11 @@ let testPlugin: TestPlugin; const testFilePath = 'test/file.md'; async function simplifiedRunAction(action: ButtonAction): Promise { - await testPlugin.api.buttonActionRunner.runAction(undefined, action, testFilePath, false, undefined); + await testPlugin.api.buttonActionRunner.runAction(undefined, action, testFilePath, { + position: undefined, + isInline: false, + isInGroup: false, + }); } const buttonActionTests: Record void> = { @@ -176,12 +180,15 @@ const buttonActionTests: Record void> = { replacement: 'no button', }, testFilePath, - false, - new NotePosition({ - // these line numbers start at 0 - lineStart: 1, - lineEnd: 1, - }), + { + position: { + // these line numbers start at 0 + lineStart: 1, + lineEnd: 1, + }, + isInline: false, + isInGroup: false, + }, ); expect(testPlugin.internal.fileSystem.readFile('test/file.md')).toBe('line1\nno button\nline3\n'); @@ -197,12 +204,15 @@ const buttonActionTests: Record void> = { replacement: 'no button', }, testFilePath, - false, - new NotePosition({ - // these line numbers start at 0 - lineStart: 1, - lineEnd: 3, - }), + { + position: { + // these line numbers start at 0 + lineStart: 1, + lineEnd: 3, + }, + isInline: false, + isInGroup: false, + }, ); expect(testPlugin.internal.fileSystem.readFile('test/file.md')).toBe('line1\nno button\nline3\n');