diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 6857a0ba..00000000 --- a/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -npm node_modules -build -main.js -Publish.js -extraTypes diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index eca9e8c1..00000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,146 +0,0 @@ -const projectConfig = require('./automation/config.json'); - -const overrides = []; - -for (const corePackage of projectConfig.corePackages) { - const patterns = []; - - for (const nonCorePackage of projectConfig.packages) { - patterns.push({ - group: [`packages/${nonCorePackage}/*`], - message: `Core package "${corePackage}" should not import from the non core "${nonCorePackage}" package.`, - }); - } - - overrides.push({ - files: [`packages/${corePackage}/src/**/*.ts`], - rules: { - 'no-restricted-imports': [ - 'error', - { - patterns: patterns, - }, - ], - }, - }); -} - -for (const nonCorePackage of projectConfig.packages) { - const patterns = []; - - for (const otherNonCorePackage of projectConfig.packages) { - if (otherNonCorePackage === nonCorePackage) { - continue; - } - patterns.push({ - group: [`packages/${otherNonCorePackage}/*`], - message: `Non core package "${nonCorePackage}" should not import from the non core "${otherNonCorePackage}" package.`, - }); - } - - overrides.push({ - files: [`packages/${nonCorePackage}/src/**/*.ts`], - rules: { - 'no-restricted-imports': [ - 'error', - { - patterns: patterns, - }, - ], - }, - }); -} - -const flatOverrides = overrides.map(o => ({ - files: o.files, - restrictedImports: o.rules['no-restricted-imports'][1].patterns.map(p => p.group).flat(), -})); - -console.log('Import restrictions:'); -console.log(flatOverrides); - -/** @type {import('eslint').Linter.Config} */ -const config = { - root: true, - parser: '@typescript-eslint/parser', - env: { - node: true, - }, - plugins: ['isaacscript', 'import', 'only-warn', 'no-relative-import-paths'], - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:@typescript-eslint/recommended-type-checked', - 'plugin:@typescript-eslint/stylistic-type-checked', - 'plugin:svelte/recommended', - 'plugin:svelte/prettier', - ], - parserOptions: { - sourceType: 'module', - tsconfigRootDir: __dirname, - ecmaVersion: 'latest', - project: ['./tsconfig.json', './packages/*/tsconfig.json'], - extraFileExtensions: ['.svelte'], - }, - rules: { - '@typescript-eslint/no-explicit-any': ['warn'], - - '@typescript-eslint/no-unused-vars': [ - 'error', - { argsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_' }, - ], - '@typescript-eslint/consistent-type-imports': [ - 'error', - { prefer: 'type-imports', fixStyle: 'separate-type-imports' }, - ], - - 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], - 'import/order': [ - 'error', - { - 'newlines-between': 'never', - alphabetize: { order: 'asc', orderImportKind: 'asc', caseInsensitive: true }, - }, - ], - - '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }], - '@typescript-eslint/restrict-template-expressions': 'off', - - 'no-relative-import-paths/no-relative-import-paths': 'error', - - '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/no-empty-function': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/explicit-function-return-type': ['warn'], - '@typescript-eslint/require-await': 'off', - }, - overrides: [ - ...overrides, - { - files: ['*.svelte'], - parser: 'svelte-eslint-parser', - parserOptions: { - parser: { - // Specify a parser for each lang. - ts: '@typescript-eslint/parser', - typescript: '@typescript-eslint/parser', - }, - }, - rules: { - '@typescript-eslint/no-unsafe-assignment': 'off', - '@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }], - '@typescript-eslint/no-unused-vars': [ - 'error', - { argsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_', varsIgnorePattern: '^_|plugin' }, - ], - - 'no-undef': 'off', - - '@typescript-eslint/prefer-nullish-coalescing': 'off', - }, - }, - ], -}; - -module.exports = config; diff --git a/bun.lockb b/bun.lockb index c72355e6..28ac1717 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..d1b54b93 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,143 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import only_warn from 'eslint-plugin-only-warn'; +import no_relative_import_paths from 'eslint-plugin-no-relative-import-paths'; +import * as plugin_import from 'eslint-plugin-import'; +import eslintPluginSvelte from 'eslint-plugin-svelte'; + +import projectConfig from './automation/config.json' with { type: 'json' }; + +/** @type {{files: string[], rules: Record}[]} */ +const overrides = []; + +for (const corePackage of projectConfig.corePackages) { + const patterns = []; + + for (const nonCorePackage of projectConfig.packages) { + patterns.push({ + group: [`packages/${nonCorePackage}/*`], + message: `Core package "${corePackage}" should not import from the non core "${nonCorePackage}" package.`, + }); + } + + overrides.push({ + files: [`packages/${corePackage}/src/**/*.ts`], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: patterns, + }, + ], + }, + }); +} + +for (const nonCorePackage of projectConfig.packages) { + const patterns = []; + + for (const otherNonCorePackage of projectConfig.packages) { + if (otherNonCorePackage === nonCorePackage) { + continue; + } + patterns.push({ + group: [`packages/${otherNonCorePackage}/*`], + message: `Non core package "${nonCorePackage}" should not import from the non core "${otherNonCorePackage}" package.`, + }); + } + + overrides.push({ + files: [`packages/${nonCorePackage}/src/**/*.ts`], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: patterns, + }, + ], + }, + }); +} + +const flatOverrides = overrides.map(o => ({ + files: o.files, + restrictedImports: o.rules['no-restricted-imports'][1].patterns.map(p => p.group).flat(), +})); + +console.log('Import restrictions:'); +console.log(flatOverrides); + +export default tseslint.config( + { + ignores: ['npm/', 'node_modules/', 'exampleVault/', 'automation/', 'main.js', '**/*.svelte', '**/*.d.ts'], + }, + ...eslintPluginSvelte.configs['flat/recommended'], + ...eslintPluginSvelte.configs['flat/prettier'], + { + files: ['packages/**/*.ts'], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + ], + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: true, + }, + }, + plugins: { + // @ts-ignore + 'only-warn': only_warn, + 'no-relative-import-paths': no_relative_import_paths, + import: plugin_import, + }, + rules: { + '@typescript-eslint/no-explicit-any': ['warn'], + + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { prefer: 'type-imports', fixStyle: 'separate-type-imports' }, + ], + + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], + 'import/order': [ + 'error', + { + 'newlines-between': 'never', + alphabetize: { order: 'asc', orderImportKind: 'asc', caseInsensitive: true }, + }, + ], + + '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }], + '@typescript-eslint/restrict-template-expressions': 'off', + + 'no-relative-import-paths/no-relative-import-paths': ['warn', { allowSameFolder: false }], + + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-inferrable-types': 'off', + '@typescript-eslint/explicit-function-return-type': ['warn'], + '@typescript-eslint/require-await': 'off', + '@typescript-eslint/no-unused-expressions': [ + 'warn', + { + allowShortCircuit: true, + }, + ], + }, + }, + ...overrides, +); diff --git a/package.json b/package.json index abc56eab..837d12d5 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "test:log": "LOG_TESTS=true bun test --conditions=browser", "format": "prettier --write --plugin prettier-plugin-svelte .", "format:check": "prettier --check --plugin prettier-plugin-svelte .", - "lint": "eslint --max-warnings=0 packages/**", - "lint:fix": "eslint --max-warnings=0 --fix packages/**", + "lint": "eslint --max-warnings=0 packages/** --no-warn-ignored", + "lint:fix": "eslint --max-warnings=0 --fix packages/** --no-warn-ignored", "svelte-check": "svelte-check --compiler-warnings \"unused-export-let:ignore\"", "types": "tsc -p \"./tsconfig.types.json\"", "check": "bun run format:check && bun run tsc && bun run svelte-check && bun run lint && bun run test", @@ -32,17 +32,15 @@ "@elysiajs/cors": "^1.1.1", "@happy-dom/global-registrator": "^14.12.3", "@tsconfig/svelte": "^5.0.4", - "@types/bun": "^1.1.12", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", + "@types/bun": "^1.1.13", "builtin-modules": "^4.0.0", - "elysia": "^1.1.23", + "elysia": "^1.1.24", "esbuild": "^0.24.0", "esbuild-plugin-copy-watch": "^2.3.1", "esbuild-svelte": "^0.8.2", - "eslint": "^8.57.1", + "eslint": "^9.14.0", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-isaacscript": "^3.12.2", + "eslint-plugin-isaacscript": "^4.0.0", "eslint-plugin-no-relative-import-paths": "^1.5.5", "eslint-plugin-only-warn": "^1.1.0", "eslint-plugin-svelte": "^2.46.0", @@ -51,17 +49,18 @@ "string-argv": "^0.3.2", "svelte-check": "^4.0.5", "svelte-preprocess": "^6.0.3", - "tslib": "^2.8.0", + "tslib": "^2.8.1", "typescript": "^5.6.3", + "typescript-eslint": "^8.13.0", "yaml": "^2.6.0" }, "dependencies": { - "@codemirror/legacy-modes": "^6.4.1", + "@codemirror/legacy-modes": "^6.4.2", "@lemons_dev/parsinom": "^0.0.12", "itertools-ts": "^1.27.1", "mathjs": "^13.2.0", "moment": "^2.30.1", - "svelte": "5.1.4", + "svelte": "^5.1.9", "zod": "^3.23.8", "zod-validation-error": "^3.4.0" }, diff --git a/packages/core/src/api/API.ts b/packages/core/src/api/API.ts index f603a952..71d28242 100644 --- a/packages/core/src/api/API.ts +++ b/packages/core/src/api/API.ts @@ -189,7 +189,7 @@ export abstract class API { filePath: string, scope: BindTargetScope | undefined, renderChildType: RenderChildType = RenderChildType.INLINE, - position?: NotePosition | undefined, + position?: NotePosition, honorExcludedSetting: boolean = true, ): FieldMountable { validateAPIArgs( @@ -247,7 +247,7 @@ export abstract class API { filePath: string, scope: BindTargetScope | undefined, renderChildType: RenderChildType = RenderChildType.INLINE, - position?: NotePosition | undefined, + position?: NotePosition, honorExcludedSetting: boolean = true, ): FieldMountable { validateAPIArgs( diff --git a/packages/core/src/config/ButtonConfig.ts b/packages/core/src/config/ButtonConfig.ts index 39b32b3a..24bf605b 100644 --- a/packages/core/src/config/ButtonConfig.ts +++ b/packages/core/src/config/ButtonConfig.ts @@ -102,7 +102,7 @@ export interface InsertIntoNoteButtonAction { templater?: boolean; } -export interface InlineJsButtonAction { +export interface InlineJSButtonAction { type: ButtonActionType.INLINE_JS; code: string; } @@ -120,7 +120,7 @@ export type ButtonAction = | ReplaceSelfButtonAction | RegexpReplaceInNoteButtonAction | InsertIntoNoteButtonAction - | InlineJsButtonAction; + | InlineJSButtonAction; export interface ButtonConfig { label: string; @@ -139,3 +139,31 @@ export interface ButtonContext { isInGroup: boolean; isInline: boolean; } + +export interface ButtonClickContext { + type: ButtonClickType; + shiftKey: boolean; + ctrlKey: boolean; + altKey: boolean; +} + +export enum ButtonClickType { + LEFT = 'left', + MIDDLE = 'middle', +} + +export interface ButtonActionMap { + [ButtonActionType.COMMAND]: CommandButtonAction; + [ButtonActionType.JS]: JSButtonAction; + [ButtonActionType.OPEN]: OpenButtonAction; + [ButtonActionType.INPUT]: InputButtonAction; + [ButtonActionType.SLEEP]: SleepButtonAction; + [ButtonActionType.TEMPLATER_CREATE_NOTE]: TemplaterCreateNoteButtonAction; + [ButtonActionType.UPDATE_METADATA]: UpdateMetadataButtonAction; + [ButtonActionType.CREATE_NOTE]: CreateNoteButtonAction; + [ButtonActionType.REPLACE_IN_NOTE]: ReplaceInNoteButtonAction; + [ButtonActionType.REPLACE_SELF]: ReplaceSelfButtonAction; + [ButtonActionType.REGEXP_REPLACE_IN_NOTE]: RegexpReplaceInNoteButtonAction; + [ButtonActionType.INSERT_INTO_NOTE]: InsertIntoNoteButtonAction; + [ButtonActionType.INLINE_JS]: InlineJSButtonAction; +} diff --git a/packages/core/src/config/ButtonConfigValidators.ts b/packages/core/src/config/ButtonConfigValidators.ts index 274ef036..211c8b6d 100644 --- a/packages/core/src/config/ButtonConfigValidators.ts +++ b/packages/core/src/config/ButtonConfigValidators.ts @@ -3,7 +3,7 @@ import type { ButtonConfig, CommandButtonAction, CreateNoteButtonAction, - InlineJsButtonAction, + InlineJSButtonAction, InputButtonAction, InsertIntoNoteButtonAction, JSButtonAction, @@ -165,7 +165,7 @@ export const V_InsertIntoNoteButtonAction = schemaForType()( +export const V_InlineJsButtonAction = schemaForType()( z.object({ type: z.literal(ButtonActionType.INLINE_JS), code: stringValidator('inlineJS', 'code', 'code string to run'), diff --git a/packages/core/src/fields/button/AbstractButtonActionConfig.ts b/packages/core/src/fields/button/AbstractButtonActionConfig.ts new file mode 100644 index 00000000..66dea10c --- /dev/null +++ b/packages/core/src/fields/button/AbstractButtonActionConfig.ts @@ -0,0 +1,29 @@ +import type { + ButtonActionType, + ButtonClickContext, + ButtonConfig, + ButtonContext, +} from 'packages/core/src/config/ButtonConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export abstract class AbstractButtonActionConfig { + actionType: ButtonActionType; + plugin: IPlugin; + + constructor(actionType: ButtonActionType, plugin: IPlugin) { + this.actionType = actionType; + this.plugin = plugin; + } + + abstract run( + config: ButtonConfig | undefined, + action: T, + filePath: string, + context: ButtonContext, + click: ButtonClickContext, + ): Promise; + + abstract create(): Required; + + abstract getActionLabel(): string; +} diff --git a/packages/core/src/fields/button/ButtonActionRunner.ts b/packages/core/src/fields/button/ButtonActionRunner.ts index d5517a01..73ec8878 100644 --- a/packages/core/src/fields/button/ButtonActionRunner.ts +++ b/packages/core/src/fields/button/ButtonActionRunner.ts @@ -1,36 +1,59 @@ import type { - ButtonAction, + ButtonActionMap, + ButtonClickContext, + ButtonClickType, ButtonConfig, ButtonContext, - CommandButtonAction, - CreateNoteButtonAction, - InlineJsButtonAction, - InputButtonAction, - InsertIntoNoteButtonAction, - JSButtonAction, - OpenButtonAction, - RegexpReplaceInNoteButtonAction, - ReplaceInNoteButtonAction, - ReplaceSelfButtonAction, - SleepButtonAction, - TemplaterCreateNoteButtonAction, - UpdateMetadataButtonAction, } from 'packages/core/src/config/ButtonConfig'; import { ButtonActionType, ButtonStyleType } from 'packages/core/src/config/ButtonConfig'; +import type { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import { CommandButtonActionConfig } from 'packages/core/src/fields/button/actions/CommandButtonActionConfig'; +import { CreateNoteButtonActionConfig } from 'packages/core/src/fields/button/actions/CreateNoteButtonActionConfig'; +import { InlineJSButtonActionConfig } from 'packages/core/src/fields/button/actions/InlineJSButtonActionConfig'; +import { InputButtonActionConfig } from 'packages/core/src/fields/button/actions/InputButtonActionConfig'; +import { InsertIntoNoteButtonActionConfig } from 'packages/core/src/fields/button/actions/InsertIntoNoteButtonActionConfig'; +import { JSButtonActionConfig } from 'packages/core/src/fields/button/actions/JSButtonActionConfig'; +import { OpenButtonActionConfig } from 'packages/core/src/fields/button/actions/OpenButtonActionConfig'; +import { RegexpReplaceInNoteButtonActionConfig } from 'packages/core/src/fields/button/actions/RegexpReplaceInNoteButtonActionConfig'; +import { ReplaceInNoteButtonActionConfig } from 'packages/core/src/fields/button/actions/ReplaceInNoteButtonActionConfig'; +import { ReplaceSelfButtonActionConfig } from 'packages/core/src/fields/button/actions/ReplaceSelfButtonActionConfig'; +import { SleepButtonActionConfig } from 'packages/core/src/fields/button/actions/SleepButtonActionConfig'; +import { TemplaterCreateNoteButtonActionConfig } from 'packages/core/src/fields/button/actions/TemplaterCreateNoteButtonActionConfig'; +import { UpdateMetadataButtonActionConfig } from 'packages/core/src/fields/button/actions/UpdateMetadataButtonActionConfig'; import type { IPlugin } from 'packages/core/src/IPlugin'; import { MDLinkParser } from 'packages/core/src/parsers/MarkdownLinkParser'; -import { ErrorLevel, MetaBindJsError, MetaBindParsingError } from 'packages/core/src/utils/errors/MetaBindErrors'; -import { parseLiteral } from 'packages/core/src/utils/Literal'; -import { ensureFileExtension, expectType, joinPath } from 'packages/core/src/utils/Utils'; +import { ErrorLevel, MetaBindParsingError } from 'packages/core/src/utils/errors/MetaBindErrors'; +type ActionContexts = { + [key in ButtonActionType]: AbstractButtonActionConfig; +}; + +// TODO: rewrite this so that each button action is its own class export class ButtonActionRunner { plugin: IPlugin; + actionContexts: ActionContexts; constructor(plugin: IPlugin) { this.plugin = plugin; + + this.actionContexts = { + [ButtonActionType.COMMAND]: new CommandButtonActionConfig(plugin), + [ButtonActionType.OPEN]: new OpenButtonActionConfig(plugin), + [ButtonActionType.JS]: new JSButtonActionConfig(plugin), + [ButtonActionType.INPUT]: new InputButtonActionConfig(plugin), + [ButtonActionType.SLEEP]: new SleepButtonActionConfig(plugin), + [ButtonActionType.TEMPLATER_CREATE_NOTE]: new TemplaterCreateNoteButtonActionConfig(plugin), + [ButtonActionType.UPDATE_METADATA]: new UpdateMetadataButtonActionConfig(plugin), + [ButtonActionType.CREATE_NOTE]: new CreateNoteButtonActionConfig(plugin), + [ButtonActionType.REPLACE_IN_NOTE]: new ReplaceInNoteButtonActionConfig(plugin), + [ButtonActionType.REPLACE_SELF]: new ReplaceSelfButtonActionConfig(plugin), + [ButtonActionType.REGEXP_REPLACE_IN_NOTE]: new RegexpReplaceInNoteButtonActionConfig(plugin), + [ButtonActionType.INSERT_INTO_NOTE]: new InsertIntoNoteButtonActionConfig(plugin), + [ButtonActionType.INLINE_JS]: new InlineJSButtonActionConfig(plugin), + }; } - resolveFilePath(filePath: string, relativeFilePath?: string | undefined): string { + resolveFilePath(filePath: string, relativeFilePath?: string): string { const targetFilePath = MDLinkParser.isLink(filePath) ? MDLinkParser.parseLink(filePath).target : filePath; const resolvedFilePath = this.plugin.internal.file.getPathByName(targetFilePath, relativeFilePath); if (resolvedFilePath === undefined) { @@ -66,13 +89,18 @@ export class ButtonActionRunner { * @param inline whether the button is inline * @param position the position of the button in the note */ - async runButtonActions(config: ButtonConfig, filePath: string, context: ButtonContext): Promise { + async runButtonActions( + config: ButtonConfig, + filePath: string, + context: ButtonContext, + click: ButtonClickContext, + ): Promise { try { if (config.action) { - await this.plugin.api.buttonActionRunner.runAction(config, config.action, filePath, context); + await this.runAction(config, config.action, filePath, context, click); } else if (config.actions) { for (const action of config.actions) { - await this.plugin.api.buttonActionRunner.runAction(config, action, filePath, context); + await this.runAction(config, action, filePath, context, click); } } else { console.warn('meta-bind | ButtonMDRC >> no action defined'); @@ -90,79 +118,8 @@ export class ButtonActionRunner { * * @param type */ - createDefaultAction(type: ButtonActionType): ButtonAction { - if (type === ButtonActionType.COMMAND) { - return { type: ButtonActionType.COMMAND, command: '' } satisfies Required; - } else if (type === ButtonActionType.OPEN) { - return { type: ButtonActionType.OPEN, link: '', newTab: true } satisfies Required; - } else if (type === ButtonActionType.JS) { - return { type: ButtonActionType.JS, file: '', args: {} } satisfies Required; - } else if (type === ButtonActionType.INPUT) { - return { type: ButtonActionType.INPUT, str: '' } satisfies Required; - } else if (type === ButtonActionType.SLEEP) { - return { type: ButtonActionType.SLEEP, ms: 0 } satisfies Required; - } else if (type === ButtonActionType.TEMPLATER_CREATE_NOTE) { - return { - type: ButtonActionType.TEMPLATER_CREATE_NOTE, - templateFile: '', - folderPath: '/', - fileName: '', - openNote: true, - openIfAlreadyExists: false, - } satisfies Required; - } else if (type === ButtonActionType.UPDATE_METADATA) { - return { - type: ButtonActionType.UPDATE_METADATA, - bindTarget: '', - evaluate: false, - value: '', - } satisfies Required; - } else if (type === ButtonActionType.CREATE_NOTE) { - return { - type: ButtonActionType.CREATE_NOTE, - folderPath: '/', - fileName: 'Untitled', - openNote: true, - openIfAlreadyExists: false, - } satisfies Required; - } else if (type === ButtonActionType.REPLACE_IN_NOTE) { - return { - type: ButtonActionType.REPLACE_IN_NOTE, - fromLine: 0, - toLine: 0, - replacement: 'Replacement text', - templater: false, - } satisfies Required; - } else if (type === ButtonActionType.REPLACE_SELF) { - return { - type: ButtonActionType.REPLACE_SELF, - replacement: 'Replacement text', - templater: false, - } satisfies Required; - } else if (type === ButtonActionType.REGEXP_REPLACE_IN_NOTE) { - return { - type: ButtonActionType.REGEXP_REPLACE_IN_NOTE, - regexp: '([A-Z])\\w+', - replacement: 'Replacement text', - regexpFlags: 'g', - } satisfies Required; - } else if (type === ButtonActionType.INSERT_INTO_NOTE) { - return { - type: ButtonActionType.INSERT_INTO_NOTE, - line: 0, - value: 'Some text', - templater: false, - } satisfies Required; - } else if (type === ButtonActionType.INLINE_JS) { - return { - type: ButtonActionType.INLINE_JS, - code: 'console.log("Hello world")', - } satisfies Required; - } - - expectType(type); - - throw new Error(`Unknown button action type: ${type}`); + createDefaultAction(type: T): Required { + return this.actionContexts[type].create(); } /** @@ -175,282 +132,27 @@ export class ButtonActionRunner { * @param inline whether the button is inline * @param position the position of the button in the note */ - async runAction( + async runAction( config: ButtonConfig | undefined, - action: ButtonAction, + action: ButtonActionMap[T], filePath: string, buttonContext: ButtonContext, + click: ButtonClickContext, ): Promise { - if (action.type === ButtonActionType.COMMAND) { - await this.runCommandAction(action); - return; - } else if (action.type === ButtonActionType.JS) { - await this.runJSAction(config, action, filePath, buttonContext); - return; - } else if (action.type === ButtonActionType.OPEN) { - await this.runOpenAction(action, filePath); - return; - } else if (action.type === ButtonActionType.INPUT) { - await this.runInputAction(action); - return; - } else if (action.type === ButtonActionType.SLEEP) { - await this.runSleepAction(action); - return; - } else if (action.type === ButtonActionType.TEMPLATER_CREATE_NOTE) { - await this.runTemplaterCreateNoteAction(action); - return; - } else if (action.type === ButtonActionType.UPDATE_METADATA) { - await this.runUpdateMetadataAction(action, filePath); - return; - } else if (action.type === ButtonActionType.CREATE_NOTE) { - await this.runCreateNoteAction(action); - return; - } else if (action.type === ButtonActionType.REPLACE_IN_NOTE) { - await this.runReplaceInNoteAction(action, filePath); - return; - } else if (action.type === ButtonActionType.REPLACE_SELF) { - await this.runReplaceSelfAction(action, filePath, buttonContext); - return; - } else if (action.type === ButtonActionType.REGEXP_REPLACE_IN_NOTE) { - 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, buttonContext); - return; - } - - expectType(action); - - throw new Error(`Unknown button action type`); - } - - async runCommandAction(action: CommandButtonAction): Promise { - this.plugin.internal.executeCommandById(action.command); - } - - async runJSAction( - config: ButtonConfig | undefined, - action: JSButtonAction, - filePath: string, - buttonContext: ButtonContext, - ): Promise { - if (!this.plugin.settings.enableJs) { - throw new MetaBindJsError({ - errorLevel: ErrorLevel.CRITICAL, - effect: "Can't run button action that requires JS evaluation.", - cause: 'JS evaluation is disabled in the plugin settings.', - }); - } - - 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(); + const actionType: T = action.type as T; + await this.actionContexts[actionType].run(config, action, filePath, buttonContext, click); } - async runOpenAction(action: OpenButtonAction, filePath: string): Promise { - MDLinkParser.parseLinkOrUrl(action.link).open(this.plugin, filePath, action.newTab ?? false); - } - - async runInputAction(action: InputButtonAction): Promise { - const el = document.activeElement; - if (el && el instanceof HTMLInputElement) { - el.setRangeText(action.str, el.selectionStart!, el.selectionEnd!, 'end'); - el.dispatchEvent(new Event('input', { bubbles: true })); - } - } - - async runSleepAction(action: SleepButtonAction): Promise { - await new Promise(resolve => setTimeout(resolve, action.ms)); - } - - async runTemplaterCreateNoteAction(action: TemplaterCreateNoteButtonAction): Promise { - if (action.openIfAlreadyExists && action.fileName) { - const filePath = ensureFileExtension(joinPath(action.folderPath ?? '', action.fileName), 'md'); - // if the file already exists, open it in the same tab - if (await this.plugin.internal.file.exists(filePath)) { - this.plugin.internal.file.open(filePath, '', false); - return; - } - } - - await this.plugin.internal.createNoteWithTemplater( - action.templateFile, - action.folderPath, - action.fileName, - action.openNote, - ); - } - - async runUpdateMetadataAction(action: UpdateMetadataButtonAction, filePath: string): Promise { - const bindTarget = this.plugin.api.bindTargetParser.fromStringAndValidate(action.bindTarget, filePath); - - if (action.evaluate) { - if (!this.plugin.settings.enableJs) { - throw new MetaBindJsError({ - errorLevel: ErrorLevel.CRITICAL, - effect: "Can't run button action that requires JS evaluation.", - cause: 'JS evaluation is disabled in the plugin settings.', - }); - } - - // eslint-disable-next-line @typescript-eslint/no-implied-eval - const func = new Function('x', 'getMetadata', `return ${action.value};`) as ( - value: unknown, - getMetadata: (bindTarget: string) => unknown, - ) => unknown; - - this.plugin.api.updateMetadata(bindTarget, value => - func(value, bindTarget => { - return this.plugin.api.getMetadata(this.plugin.api.parseBindTarget(bindTarget, filePath)); - }), - ); - } else { - this.plugin.api.setMetadata(bindTarget, parseLiteral(action.value)); - } - } - - async runCreateNoteAction(action: CreateNoteButtonAction): Promise { - if (action.openIfAlreadyExists) { - const filePath = ensureFileExtension(joinPath(action.folderPath ?? '', action.fileName), 'md'); - // if the file already exists, open it in the same tab - if (await this.plugin.internal.file.exists(filePath)) { - this.plugin.internal.file.open(filePath, '', false); - return; - } - } - - await this.plugin.internal.file.create( - action.folderPath ?? '', - action.fileName, - 'md', - action.openNote ?? false, - ); + getActionLabel(type: T): string { + return this.actionContexts[type].getActionLabel(); } - async runReplaceInNoteAction(action: ReplaceInNoteButtonAction, filePath: string): Promise { - if (action.fromLine > action.toLine) { - throw new Error('From line cannot be greater than to line'); - } - - const replacement = action.templater - ? await this.plugin.internal.evaluateTemplaterTemplate(this.resolveFilePath(action.replacement), filePath) - : action.replacement; - - await this.plugin.internal.file.atomicModify(filePath, content => { - let splitContent = content.split('\n'); - - if (action.fromLine < 0 || action.toLine > splitContent.length + 1) { - throw new Error('Line numbers out of bounds'); - } - - splitContent = [ - ...splitContent.slice(0, action.fromLine - 1), - replacement, - ...splitContent.slice(action.toLine), - ]; - - return splitContent.join('\n'); - }); - } - - async runReplaceSelfAction( - action: ReplaceSelfButtonAction, - filePath: string, - buttonContext: ButtonContext, - ): Promise { - if (buttonContext.isInline) { - throw new Error('Replace self action not supported for inline buttons'); - } - - if (buttonContext.position === undefined) { - throw new Error('Position of the button in the note is unknown'); - } - - if (buttonContext.position.lineStart > buttonContext.position.lineEnd) { - throw new Error('Position of the button in the note is invalid'); - } - - const position = buttonContext.position; - - const replacement = action.templater - ? await this.plugin.internal.evaluateTemplaterTemplate(this.resolveFilePath(action.replacement), filePath) - : action.replacement; - - await this.plugin.internal.file.atomicModify(filePath, content => { - let splitContent = content.split('\n'); - - if (position.lineStart < 0 || position.lineEnd > splitContent.length + 1) { - throw new Error('Position of the button in the note is out of bounds'); - } - - splitContent = [ - ...splitContent.slice(0, position.lineStart), - replacement, - ...splitContent.slice(position.lineEnd + 1), - ]; - - return splitContent.join('\n'); - }); - } - - async runRegexpReplaceInNoteAction(action: RegexpReplaceInNoteButtonAction, filePath: string): Promise { - if (action.regexp === '') { - throw new Error('Regexp cannot be empty'); - } - - await this.plugin.internal.file.atomicModify(filePath, content => { - return content.replace(new RegExp(action.regexp, action.regexpFlags ?? 'g'), action.replacement); - }); - } - - async runInsertIntoNoteAction(action: InsertIntoNoteButtonAction, filePath: string): Promise { - const insertString = action.templater - ? await this.plugin.internal.evaluateTemplaterTemplate(this.resolveFilePath(action.value), filePath) - : action.value; - - await this.plugin.internal.file.atomicModify(filePath, content => { - let splitContent = content.split('\n'); - - if (action.line < 1 || action.line > splitContent.length + 1) { - throw new Error('Line number out of bounds'); - } - - splitContent = [ - ...splitContent.slice(0, action.line - 1), - insertString, - ...splitContent.slice(action.line - 1), - ]; - - return splitContent.join('\n'); - }); - } - - async runInlineJsAction( - config: ButtonConfig | undefined, - action: InlineJsButtonAction, - filePath: string, - buttonContext: ButtonContext, - ): Promise { - if (!this.plugin.settings.enableJs) { - throw new MetaBindJsError({ - errorLevel: ErrorLevel.CRITICAL, - effect: "Can't run button action that requires JS evaluation.", - cause: 'JS evaluation is disabled in the plugin settings.', - }); - } - - const configOverrides: Record = { - buttonConfig: structuredClone(config), - buttonContext: structuredClone(buttonContext), + mouseEventToClickContext(event: MouseEvent, type: ButtonClickType): ButtonClickContext { + return { + type: type, + shiftKey: event.shiftKey, + ctrlKey: event.ctrlKey, + altKey: event.altKey, }; - 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 0589c554..5aaf69bb 100644 --- a/packages/core/src/fields/button/ButtonField.ts +++ b/packages/core/src/fields/button/ButtonField.ts @@ -1,6 +1,7 @@ import type { NotePosition } from 'packages/core/src/config/APIConfigs'; import { RenderChildType } from 'packages/core/src/config/APIConfigs'; import type { ButtonConfig, ButtonContext } from 'packages/core/src/config/ButtonConfig'; +import { ButtonClickType } 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'; @@ -67,11 +68,20 @@ export class ButtonField extends Mountable { variant: this.config.style, label: this.config.label, tooltip: isTruthy(this.config.tooltip) ? this.config.tooltip : this.config.label, - onclick: async (): Promise => { + onclick: async (event: MouseEvent): Promise => { await this.plugin.api.buttonActionRunner.runButtonActions( this.config, this.filePath, this.getContext(), + this.plugin.api.buttonActionRunner.mouseEventToClickContext(event, ButtonClickType.LEFT), + ); + }, + onauxclick: async (event: MouseEvent): Promise => { + await this.plugin.api.buttonActionRunner.runButtonActions( + this.config, + this.filePath, + this.getContext(), + this.plugin.api.buttonActionRunner.mouseEventToClickContext(event, ButtonClickType.MIDDLE), ); }, }, diff --git a/packages/core/src/fields/button/actions/CommandButtonActionConfig.ts b/packages/core/src/fields/button/actions/CommandButtonActionConfig.ts new file mode 100644 index 00000000..5b3e242b --- /dev/null +++ b/packages/core/src/fields/button/actions/CommandButtonActionConfig.ts @@ -0,0 +1,33 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + CommandButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export class CommandButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.COMMAND, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: CommandButtonAction, + _filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + this.plugin.internal.executeCommandById(action.command); + } + + create(): Required { + return { type: ButtonActionType.COMMAND, command: '' }; + } + + getActionLabel(): string { + return 'Run a command'; + } +} diff --git a/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts b/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts new file mode 100644 index 00000000..619cfd50 --- /dev/null +++ b/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts @@ -0,0 +1,54 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + CreateNoteButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; +import { ensureFileExtension, joinPath } from 'packages/core/src/utils/Utils'; + +export class CreateNoteButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.CREATE_NOTE, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: CreateNoteButtonAction, + _filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + if (action.openIfAlreadyExists) { + const filePath = ensureFileExtension(joinPath(action.folderPath ?? '', action.fileName), 'md'); + // if the file already exists, open it in the same tab + if (await this.plugin.internal.file.exists(filePath)) { + this.plugin.internal.file.open(filePath, '', false); + return; + } + } + + await this.plugin.internal.file.create( + action.folderPath ?? '', + action.fileName, + 'md', + action.openNote ?? false, + ); + } + + create(): Required { + return { + type: ButtonActionType.CREATE_NOTE, + folderPath: '/', + fileName: 'Untitled', + openNote: true, + openIfAlreadyExists: false, + }; + } + + getActionLabel(): string { + return 'Create a new note'; + } +} diff --git a/packages/core/src/fields/button/actions/InlineJSButtonActionConfig.ts b/packages/core/src/fields/button/actions/InlineJSButtonActionConfig.ts new file mode 100644 index 00000000..8cb20df4 --- /dev/null +++ b/packages/core/src/fields/button/actions/InlineJSButtonActionConfig.ts @@ -0,0 +1,51 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + InlineJSButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; +import { ErrorLevel, MetaBindJsError } from 'packages/core/src/utils/errors/MetaBindErrors'; + +export class InlineJSButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.INLINE_JS, plugin); + } + + async run( + config: ButtonConfig | undefined, + action: InlineJSButtonAction, + filePath: string, + context: ButtonContext, + click: ButtonClickContext, + ): Promise { + if (!this.plugin.settings.enableJs) { + throw new MetaBindJsError({ + errorLevel: ErrorLevel.CRITICAL, + effect: "Can't run button action that requires JS evaluation.", + cause: 'JS evaluation is disabled in the plugin settings.', + }); + } + + const configOverrides: Record = { + buttonConfig: structuredClone(config), + buttonContext: structuredClone(context), + click: structuredClone(click), + }; + const unloadCallback = await this.plugin.internal.jsEngineRunCode(action.code, filePath, configOverrides); + unloadCallback(); + } + + create(): Required { + return { + type: ButtonActionType.INLINE_JS, + code: 'console.log("Hello world")', + }; + } + + getActionLabel(): string { + return 'Run JavaScript code'; + } +} diff --git a/packages/core/src/fields/button/actions/InputButtonActionConfig.ts b/packages/core/src/fields/button/actions/InputButtonActionConfig.ts new file mode 100644 index 00000000..8ad15dab --- /dev/null +++ b/packages/core/src/fields/button/actions/InputButtonActionConfig.ts @@ -0,0 +1,37 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + InputButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export class InputButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.INPUT, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: InputButtonAction, + _filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + const el = document.activeElement; + if (el && el instanceof HTMLInputElement) { + el.setRangeText(action.str, el.selectionStart!, el.selectionEnd!, 'end'); + el.dispatchEvent(new Event('input', { bubbles: true })); + } + } + + create(): Required { + return { type: ButtonActionType.INPUT, str: '' }; + } + + getActionLabel(): string { + return 'Insert text at cursor'; + } +} diff --git a/packages/core/src/fields/button/actions/InsertIntoNoteButtonActionConfig.ts b/packages/core/src/fields/button/actions/InsertIntoNoteButtonActionConfig.ts new file mode 100644 index 00000000..17a2fc53 --- /dev/null +++ b/packages/core/src/fields/button/actions/InsertIntoNoteButtonActionConfig.ts @@ -0,0 +1,59 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + InsertIntoNoteButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export class InsertIntoNoteButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.INSERT_INTO_NOTE, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: InsertIntoNoteButtonAction, + filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + const insertString = action.templater + ? await this.plugin.internal.evaluateTemplaterTemplate( + this.plugin.api.buttonActionRunner.resolveFilePath(action.value), + filePath, + ) + : action.value; + + await this.plugin.internal.file.atomicModify(filePath, content => { + let splitContent = content.split('\n'); + + if (action.line < 1 || action.line > splitContent.length + 1) { + throw new Error('Line number out of bounds'); + } + + splitContent = [ + ...splitContent.slice(0, action.line - 1), + insertString, + ...splitContent.slice(action.line - 1), + ]; + + return splitContent.join('\n'); + }); + } + + create(): Required { + return { + type: ButtonActionType.INSERT_INTO_NOTE, + line: 0, + value: 'Some text', + templater: false, + }; + } + + getActionLabel(): string { + return 'Insert text into the note'; + } +} diff --git a/packages/core/src/fields/button/actions/JSButtonActionConfig.ts b/packages/core/src/fields/button/actions/JSButtonActionConfig.ts new file mode 100644 index 00000000..65675961 --- /dev/null +++ b/packages/core/src/fields/button/actions/JSButtonActionConfig.ts @@ -0,0 +1,49 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + JSButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; +import { ErrorLevel, MetaBindJsError } from 'packages/core/src/utils/errors/MetaBindErrors'; + +export class JSButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.JS, plugin); + } + + async run( + config: ButtonConfig | undefined, + action: JSButtonAction, + filePath: string, + context: ButtonContext, + click: ButtonClickContext, + ): Promise { + if (!this.plugin.settings.enableJs) { + throw new MetaBindJsError({ + errorLevel: ErrorLevel.CRITICAL, + effect: "Can't run button action that requires JS evaluation.", + cause: 'JS evaluation is disabled in the plugin settings.', + }); + } + + const configOverrides: Record = { + buttonConfig: structuredClone(config), + args: structuredClone(action.args), + buttonContext: structuredClone(context), + click: structuredClone(click), + }; + const unloadCallback = await this.plugin.internal.jsEngineRunFile(action.file, filePath, configOverrides); + unloadCallback(); + } + + create(): Required { + return { type: ButtonActionType.JS, file: '', args: {} }; + } + + getActionLabel(): string { + return 'Run a JavaScript file'; + } +} diff --git a/packages/core/src/fields/button/actions/OpenButtonActionConfig.ts b/packages/core/src/fields/button/actions/OpenButtonActionConfig.ts new file mode 100644 index 00000000..98c4ed0d --- /dev/null +++ b/packages/core/src/fields/button/actions/OpenButtonActionConfig.ts @@ -0,0 +1,34 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + OpenButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; +import { MDLinkParser } from 'packages/core/src/parsers/MarkdownLinkParser'; + +export class OpenButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.OPEN, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: OpenButtonAction, + filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + MDLinkParser.parseLinkOrUrl(action.link).open(this.plugin, filePath, action.newTab ?? false); + } + + create(): Required { + return { type: ButtonActionType.OPEN, link: '', newTab: true }; + } + + getActionLabel(): string { + return 'Open a link'; + } +} diff --git a/packages/core/src/fields/button/actions/RegexpReplaceInNoteButtonActionConfig.ts b/packages/core/src/fields/button/actions/RegexpReplaceInNoteButtonActionConfig.ts new file mode 100644 index 00000000..e222425b --- /dev/null +++ b/packages/core/src/fields/button/actions/RegexpReplaceInNoteButtonActionConfig.ts @@ -0,0 +1,44 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + RegexpReplaceInNoteButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export class RegexpReplaceInNoteButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.REGEXP_REPLACE_IN_NOTE, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: RegexpReplaceInNoteButtonAction, + filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + if (action.regexp === '') { + throw new Error('Regexp cannot be empty'); + } + + await this.plugin.internal.file.atomicModify(filePath, content => { + return content.replace(new RegExp(action.regexp, action.regexpFlags ?? 'g'), action.replacement); + }); + } + + create(): Required { + return { + type: ButtonActionType.REGEXP_REPLACE_IN_NOTE, + regexp: '([A-Z])\\w+', + replacement: 'Replacement text', + regexpFlags: 'g', + }; + } + + getActionLabel(): string { + return 'Replace text in note using regexp'; + } +} diff --git a/packages/core/src/fields/button/actions/ReplaceInNoteButtonActionConfig.ts b/packages/core/src/fields/button/actions/ReplaceInNoteButtonActionConfig.ts new file mode 100644 index 00000000..d33fc2a5 --- /dev/null +++ b/packages/core/src/fields/button/actions/ReplaceInNoteButtonActionConfig.ts @@ -0,0 +1,64 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + ReplaceInNoteButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export class ReplaceInNoteButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.REPLACE_IN_NOTE, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: ReplaceInNoteButtonAction, + filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + if (action.fromLine > action.toLine) { + throw new Error('From line cannot be greater than to line'); + } + + const replacement = action.templater + ? await this.plugin.internal.evaluateTemplaterTemplate( + this.plugin.api.buttonActionRunner.resolveFilePath(action.replacement), + filePath, + ) + : action.replacement; + + await this.plugin.internal.file.atomicModify(filePath, content => { + let splitContent = content.split('\n'); + + if (action.fromLine < 0 || action.toLine > splitContent.length + 1) { + throw new Error('Line numbers out of bounds'); + } + + splitContent = [ + ...splitContent.slice(0, action.fromLine - 1), + replacement, + ...splitContent.slice(action.toLine), + ]; + + return splitContent.join('\n'); + }); + } + + create(): Required { + return { + type: ButtonActionType.REPLACE_IN_NOTE, + fromLine: 0, + toLine: 0, + replacement: 'Replacement text', + templater: false, + }; + } + + getActionLabel(): string { + return 'Replace text in note'; + } +} diff --git a/packages/core/src/fields/button/actions/ReplaceSelfButtonActionConfig.ts b/packages/core/src/fields/button/actions/ReplaceSelfButtonActionConfig.ts new file mode 100644 index 00000000..e83d3ec6 --- /dev/null +++ b/packages/core/src/fields/button/actions/ReplaceSelfButtonActionConfig.ts @@ -0,0 +1,72 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + ReplaceSelfButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export class ReplaceSelfButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.REPLACE_SELF, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: ReplaceSelfButtonAction, + filePath: string, + context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + if (context.isInline) { + throw new Error('Replace self action not supported for inline buttons'); + } + + if (context.position === undefined) { + throw new Error('Position of the button in the note is unknown'); + } + + if (context.position.lineStart > context.position.lineEnd) { + throw new Error('Position of the button in the note is invalid'); + } + + const position = context.position; + + const replacement = action.templater + ? await this.plugin.internal.evaluateTemplaterTemplate( + this.plugin.api.buttonActionRunner.resolveFilePath(action.replacement), + filePath, + ) + : action.replacement; + + await this.plugin.internal.file.atomicModify(filePath, content => { + let splitContent = content.split('\n'); + + if (position.lineStart < 0 || position.lineEnd > splitContent.length + 1) { + throw new Error('Position of the button in the note is out of bounds'); + } + + splitContent = [ + ...splitContent.slice(0, position.lineStart), + replacement, + ...splitContent.slice(position.lineEnd + 1), + ]; + + return splitContent.join('\n'); + }); + } + + create(): Required { + return { + type: ButtonActionType.REPLACE_SELF, + replacement: 'Replacement text', + templater: false, + }; + } + + getActionLabel(): string { + return 'Replace button with text'; + } +} diff --git a/packages/core/src/fields/button/actions/SleepButtonActionConfig.ts b/packages/core/src/fields/button/actions/SleepButtonActionConfig.ts new file mode 100644 index 00000000..95a7f491 --- /dev/null +++ b/packages/core/src/fields/button/actions/SleepButtonActionConfig.ts @@ -0,0 +1,33 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + SleepButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export class SleepButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.SLEEP, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: SleepButtonAction, + _filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + await new Promise(resolve => setTimeout(resolve, action.ms)); + } + + create(): Required { + return { type: ButtonActionType.SLEEP, ms: 0 }; + } + + getActionLabel(): string { + return 'Sleep for some time'; + } +} diff --git a/packages/core/src/fields/button/actions/TemplaterCreateNoteButtonActionConfig.ts b/packages/core/src/fields/button/actions/TemplaterCreateNoteButtonActionConfig.ts new file mode 100644 index 00000000..3f3f1712 --- /dev/null +++ b/packages/core/src/fields/button/actions/TemplaterCreateNoteButtonActionConfig.ts @@ -0,0 +1,55 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + TemplaterCreateNoteButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; +import { ensureFileExtension, joinPath } from 'packages/core/src/utils/Utils'; + +export class TemplaterCreateNoteButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.TEMPLATER_CREATE_NOTE, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: TemplaterCreateNoteButtonAction, + _filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + if (action.openIfAlreadyExists && action.fileName) { + const filePath = ensureFileExtension(joinPath(action.folderPath ?? '', action.fileName), 'md'); + // if the file already exists, open it in the same tab + if (await this.plugin.internal.file.exists(filePath)) { + this.plugin.internal.file.open(filePath, '', false); + return; + } + } + + await this.plugin.internal.createNoteWithTemplater( + action.templateFile, + action.folderPath, + action.fileName, + action.openNote, + ); + } + + create(): Required { + return { + type: ButtonActionType.TEMPLATER_CREATE_NOTE, + templateFile: '', + folderPath: '/', + fileName: '', + openNote: true, + openIfAlreadyExists: false, + }; + } + + getActionLabel(): string { + return 'Create a new note using Templater'; + } +} diff --git a/packages/core/src/fields/button/actions/UpdateMetadataButtonActionConfig.ts b/packages/core/src/fields/button/actions/UpdateMetadataButtonActionConfig.ts new file mode 100644 index 00000000..03863832 --- /dev/null +++ b/packages/core/src/fields/button/actions/UpdateMetadataButtonActionConfig.ts @@ -0,0 +1,64 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + UpdateMetadataButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; +import { ErrorLevel, MetaBindJsError } from 'packages/core/src/utils/errors/MetaBindErrors'; +import { parseLiteral } from 'packages/core/src/utils/Literal'; + +export class UpdateMetadataButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.UPDATE_METADATA, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: UpdateMetadataButtonAction, + filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + const bindTarget = this.plugin.api.bindTargetParser.fromStringAndValidate(action.bindTarget, filePath); + + if (action.evaluate) { + if (!this.plugin.settings.enableJs) { + throw new MetaBindJsError({ + errorLevel: ErrorLevel.CRITICAL, + effect: "Can't run button action that requires JS evaluation.", + cause: 'JS evaluation is disabled in the plugin settings.', + }); + } + + // eslint-disable-next-line @typescript-eslint/no-implied-eval + const func = new Function('x', 'getMetadata', `return ${action.value};`) as ( + value: unknown, + getMetadata: (bindTarget: string) => unknown, + ) => unknown; + + this.plugin.api.updateMetadata(bindTarget, value => + func(value, bindTarget => { + return this.plugin.api.getMetadata(this.plugin.api.parseBindTarget(bindTarget, filePath)); + }), + ); + } else { + this.plugin.api.setMetadata(bindTarget, parseLiteral(action.value)); + } + } + + create(): Required { + return { + type: ButtonActionType.UPDATE_METADATA, + bindTarget: '', + evaluate: false, + value: '', + }; + } + + getActionLabel(): string { + return 'Update metadata'; + } +} diff --git a/packages/core/src/fields/inputFields/InputFieldSvelteWrapper.ts b/packages/core/src/fields/inputFields/InputFieldSvelteWrapper.ts index 6c8f6faf..0885ad79 100644 --- a/packages/core/src/fields/inputFields/InputFieldSvelteWrapper.ts +++ b/packages/core/src/fields/inputFields/InputFieldSvelteWrapper.ts @@ -42,7 +42,6 @@ export class InputFieldSvelteWrapper extends Noti * @param value */ public setValue(value: Value): void { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call this.svelteComponentInstance?.setValue(value); } diff --git a/packages/core/src/fields/viewFields/fields/MathVF.ts b/packages/core/src/fields/viewFields/fields/MathVF.ts index 80a64e17..bf9e3601 100644 --- a/packages/core/src/fields/viewFields/fields/MathVF.ts +++ b/packages/core/src/fields/viewFields/fields/MathVF.ts @@ -79,7 +79,6 @@ export class MathVF extends AbstractViewField { const context = this.buildMathJSContext(); try { - // eslint-disable-next-line const value: unknown = this.expression.evaluate(context); return typeof value === 'string' ? parseLiteral(value) : value; } catch (e) { diff --git a/packages/core/src/metadata/ComputedMetadataSubscription.ts b/packages/core/src/metadata/ComputedMetadataSubscription.ts index 9fc32a8d..8db196ca 100644 --- a/packages/core/src/metadata/ComputedMetadataSubscription.ts +++ b/packages/core/src/metadata/ComputedMetadataSubscription.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents import type { IMetadataSubscription } from 'packages/core/src/metadata/IMetadataSubscription'; import type { MetadataManager } from 'packages/core/src/metadata/MetadataManager'; import type { MetadataSubscription } from 'packages/core/src/metadata/MetadataSubscription'; diff --git a/packages/core/src/metadata/InternalMetadataSources.ts b/packages/core/src/metadata/InternalMetadataSources.ts index 24c16d1a..167ed83e 100644 --- a/packages/core/src/metadata/InternalMetadataSources.ts +++ b/packages/core/src/metadata/InternalMetadataSources.ts @@ -29,7 +29,7 @@ export class InternalMetadataSource extends FilePathMetadataSource { + public syncExternal(_cacheItem: FilePathMetadataCacheItem): void { // Do nothing } } diff --git a/packages/core/src/metadata/MetadataSource.ts b/packages/core/src/metadata/MetadataSource.ts index c83bea10..79204045 100644 --- a/packages/core/src/metadata/MetadataSource.ts +++ b/packages/core/src/metadata/MetadataSource.ts @@ -290,7 +290,6 @@ export abstract class FilePathMetadataSource(actionType); - - return 'CHANGE ME'; + return plugin.api.buttonActionRunner.getActionLabel(actionType); } function openActionContextMenu(index: number, e: MouseEvent): void { diff --git a/packages/core/src/modals/modalContents/buttonBuilder/InlineJsActionSettings.svelte b/packages/core/src/modals/modalContents/buttonBuilder/InlineJsActionSettings.svelte index 64916e18..2976bef8 100644 --- a/packages/core/src/modals/modalContents/buttonBuilder/InlineJsActionSettings.svelte +++ b/packages/core/src/modals/modalContents/buttonBuilder/InlineJsActionSettings.svelte @@ -1,5 +1,5 @@ diff --git a/packages/core/src/parsers/bindTargetParser/BindTargetParser.ts b/packages/core/src/parsers/bindTargetParser/BindTargetParser.ts index aabcf2a4..0eb490bb 100644 --- a/packages/core/src/parsers/bindTargetParser/BindTargetParser.ts +++ b/packages/core/src/parsers/bindTargetParser/BindTargetParser.ts @@ -23,11 +23,7 @@ export class BindTargetParser { return runParser(P_BindTarget, declarationString); } - fromStringAndValidate( - bindTargetString: string, - filePath: string, - scope?: BindTargetScope | undefined, - ): BindTargetDeclaration { + fromStringAndValidate(bindTargetString: string, filePath: string, scope?: BindTargetScope): BindTargetDeclaration { return this.validate(bindTargetString, this.fromString(bindTargetString), filePath, scope); } @@ -56,7 +52,7 @@ export class BindTargetParser { fullDeclaration: string | undefined, unvalidatedBindTargetDeclaration: UnvalidatedBindTargetDeclaration, filePath: string, - scope?: BindTargetScope | undefined, + scope?: BindTargetScope, ): BindTargetDeclaration { const bindTargetDeclaration: BindTargetDeclaration = {} as BindTargetDeclaration; @@ -108,7 +104,7 @@ export class BindTargetParser { return source.resolveBindTargetScope(bindTargetDeclaration, scope, this); } - public resolveScope(bindTarget: BindTargetDeclaration, scope?: BindTargetScope | undefined): BindTargetDeclaration { + public resolveScope(bindTarget: BindTargetDeclaration, scope?: BindTargetScope): BindTargetDeclaration { if (scope === undefined) { throw new ParsingValidationError( ErrorLevel.ERROR, diff --git a/packages/core/src/parsers/inputFieldParser/InputFieldDeclarationValidator.ts b/packages/core/src/parsers/inputFieldParser/InputFieldDeclarationValidator.ts index 93ab469a..9c4e7a6e 100644 --- a/packages/core/src/parsers/inputFieldParser/InputFieldDeclarationValidator.ts +++ b/packages/core/src/parsers/inputFieldParser/InputFieldDeclarationValidator.ts @@ -50,7 +50,6 @@ export class InputFieldDeclarationValidator { const inputFieldType = this.unvalidatedDeclaration.inputFieldType; for (const entry of Object.entries(InputFieldType)) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison if (entry[1] === inputFieldType?.value) { return entry[1]; } diff --git a/packages/core/src/parsers/viewFieldParser/ViewFieldParser.ts b/packages/core/src/parsers/viewFieldParser/ViewFieldParser.ts index c6e5696c..c5960bb4 100644 --- a/packages/core/src/parsers/viewFieldParser/ViewFieldParser.ts +++ b/packages/core/src/parsers/viewFieldParser/ViewFieldParser.ts @@ -41,11 +41,7 @@ export class ViewFieldParser { }; } - fromStringAndValidate( - declarationString: string, - filePath: string, - scope?: BindTargetScope | undefined, - ): ViewFieldDeclaration { + fromStringAndValidate(declarationString: string, filePath: string, scope?: BindTargetScope): ViewFieldDeclaration { return this.validate(this.fromString(declarationString), filePath, scope); } @@ -76,7 +72,7 @@ export class ViewFieldParser { fromSimpleDeclarationAndValidate( simpleDeclaration: SimpleViewFieldDeclaration, filePath: string, - scope?: BindTargetScope | undefined, + scope?: BindTargetScope, ): ViewFieldDeclaration { return this.validate(this.fromSimpleDeclaration(simpleDeclaration), filePath, scope); } @@ -96,7 +92,7 @@ export class ViewFieldParser { validate( unvalidatedDeclaration: UnvalidatedViewFieldDeclaration, filePath: string, - scope?: BindTargetScope | undefined, + scope?: BindTargetScope, ): ViewFieldDeclaration { const validator = new ViewFieldDeclarationValidator(unvalidatedDeclaration, filePath, this.plugin); diff --git a/packages/core/src/utils/ZodUtils.ts b/packages/core/src/utils/ZodUtils.ts index 531098d3..e46b7a93 100644 --- a/packages/core/src/utils/ZodUtils.ts +++ b/packages/core/src/utils/ZodUtils.ts @@ -6,8 +6,7 @@ export function oneOf< A, K1 extends Extract, K2 extends Extract, - R extends A & - ((Required> & { [P in K2]: undefined }) | (Required> & { [P in K1]: undefined })), + R extends A & ((Required> & Record) | (Required> & Record)), >(key1: K1, key2: K2): (arg: A, ctx: RefinementCtx) => arg is R { return (arg, ctx): arg is R => { if ((arg[key1] === undefined) === (arg[key2] === undefined)) { diff --git a/packages/core/src/utils/components/ButtonComponent.svelte b/packages/core/src/utils/components/ButtonComponent.svelte index 90d9ed7e..64450f71 100644 --- a/packages/core/src/utils/components/ButtonComponent.svelte +++ b/packages/core/src/utils/components/ButtonComponent.svelte @@ -1,7 +1,7 @@