From cd0627bfec3598ba0b7a8304145e3a746aad75e1 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 4 Jan 2026 19:37:27 +0100 Subject: [PATCH] =?UTF-8?q?Macros=202.0=20[=E2=9E=95Macros]=20-=20Add=20`{?= =?UTF-8?q?{hasExtension}}`=20macro,=20and=20refactor=20extension=20lookup?= =?UTF-8?q?=20logic=20(#4948)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add {{hasExtension}} macro and refactor extension lookup logic - Move findExtension() from extensions-slashcommands.js to extensions.js and export it - Update findExtension() to return object with name and enabled properties instead of just name string - Add {{hasExtension}} macro to check if an extension is enabled - Update /extension-state and /extension-exists commands to use refactored findExtension() - Remove duplicate findExtension() implementation from extensions-slashcommands.js * Refactor extension action callbacks to use extension object instead of separate name and enabled properties * fix eslint --- public/scripts/extensions-slashcommands.js | 59 +++++++------------ public/scripts/extensions.js | 17 +++++- .../macros/definitions/state-macros.js | 17 ++++++ 3 files changed, 55 insertions(+), 38 deletions(-) diff --git a/public/scripts/extensions-slashcommands.js b/public/scripts/extensions-slashcommands.js index 8bf957303..e32f39451 100644 --- a/public/scripts/extensions-slashcommands.js +++ b/public/scripts/extensions-slashcommands.js @@ -1,11 +1,11 @@ -import { disableExtension, enableExtension, extension_settings, extensionNames } from './extensions.js'; +import { disableExtension, enableExtension, extensionNames, findExtension } from './extensions.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; -import { equalsIgnoreCaseAndAccents, isFalseBoolean, isTrueBoolean } from './utils.js'; +import { isFalseBoolean, isTrueBoolean } from './utils.js'; /** * @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension @@ -22,30 +22,28 @@ function getExtensionActionCallback(action) { } const reload = !isFalseBoolean(args?.reload?.toString()); - const internalExtensionName = findExtension(extensionName); - if (!internalExtensionName) { + const extension = findExtension(extensionName); + if (!extension) { toastr.warning(`Extension ${extensionName} does not exist.`); return ''; } - const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName); - - if (action === 'enable' && isEnabled) { - toastr.info(`Extension ${extensionName} is already enabled.`); - return internalExtensionName; + if (action === 'enable' && extension.enabled) { + toastr.info(`Extension ${extension.name} is already enabled.`); + return extension.name; } - if (action === 'disable' && !isEnabled) { - toastr.info(`Extension ${extensionName} is already disabled.`); - return internalExtensionName; + if (action === 'disable' && !extension.enabled) { + toastr.info(`Extension ${extension.name} is already disabled.`); + return extension.name; } if (action === 'toggle') { - action = isEnabled ? 'disable' : 'enable'; + action = extension.enabled ? 'disable' : 'enable'; } if (reload) { - toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extensionName} and reloading...`); + toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extension.name} and reloading...`); // Clear input, so it doesn't stay because the command didn't "finish", // and wait for a bit to both show the toast and let the clear bubble through. @@ -54,35 +52,23 @@ function getExtensionActionCallback(action) { } if (action === 'enable') { - await enableExtension(internalExtensionName, reload); + await enableExtension(extension.name, reload); } else { - await disableExtension(internalExtensionName, reload); + await disableExtension(extension.name, reload); } - toastr.success(`Extension ${extensionName} ${action}d.`); + toastr.success(`Extension ${extension.name} ${action}d.`); - console.info(`Extension ${action}ed: ${extensionName}`); + console.info(`Extension ${action}ed: ${extension.name}`); if (!reload) { console.info('Reload not requested, so page needs to be reloaded manually for changes to take effect.'); } - return internalExtensionName; + return extension.name; }; } -/** - * Finds an extension by name, allowing omission of the "third-party/" prefix. - * - * @param {string} name - The name of the extension to find - * @returns {string?} - The matched extension name or undefined if not found - */ -function findExtension(name) { - return extensionNames.find(extName => { - return equalsIgnoreCaseAndAccents(extName, name) || equalsIgnoreCaseAndAccents(extName, `third-party/${name}`); - }); -} - /** * Provides an array of SlashCommandEnumValue objects based on the extension names. * Each object contains the name of the extension and a description indicating if it is a third-party extension. @@ -244,14 +230,13 @@ export function registerExtensionSlashCommands() { name: 'extension-state', callback: async (_, extensionName) => { if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.'); - const internalExtensionName = findExtension(extensionName); - if (!internalExtensionName) { + const extension = findExtension(extensionName); + if (!extension) { toastr.warning(`Extension ${extensionName} does not exist.`); return ''; } - const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName); - return String(isEnabled); + return String(extension.enabled); }, returns: 'The state of the extension, whether it is enabled.', unnamedArgumentList: [ @@ -282,8 +267,8 @@ export function registerExtensionSlashCommands() { aliases: ['extension-installed'], callback: async (_, extensionName) => { if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.'); - const exists = findExtension(extensionName) !== undefined; - return exists ? 'true' : 'false'; + const extension = findExtension(extensionName); + return extension !== null ? 'true' : 'false'; }, returns: 'Whether the extension exists and is installed.', unnamedArgumentList: [ diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index c3cdc1bda..0845b3c14 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -4,7 +4,7 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque import { showLoader } from './loader.js'; import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; import { renderTemplate, renderTemplateAsync } from './templates.js'; -import { delay, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js'; +import { delay, equalsIgnoreCaseAndAccents, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js'; import { getContext } from './st-context.js'; import { isAdmin } from './user.js'; import { addLocaleData, getCurrentLocale, t } from './i18n.js'; @@ -331,6 +331,21 @@ export async function disableExtension(name, reload = true) { } } +/** + * Finds an extension by name, allowing omission of the "third-party/" prefix. + * + * @param {string} name - The name of the extension to find + * @returns {{name: string, enabled: boolean}|null} Object with name and enabled properties, or null if not found + */ +export function findExtension(name) { + const internalExtensionName = extensionNames.find(extName => { + return equalsIgnoreCaseAndAccents(extName, name) || equalsIgnoreCaseAndAccents(extName, `third-party/${name}`); + }); + if (!internalExtensionName) return null; + const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName); + return { name: internalExtensionName, enabled: isEnabled }; +} + /** * Loads manifest.json files for extensions. * @param {string[]} names Array of extension names diff --git a/public/scripts/macros/definitions/state-macros.js b/public/scripts/macros/definitions/state-macros.js index 8537472d5..8a4b430e1 100644 --- a/public/scripts/macros/definitions/state-macros.js +++ b/public/scripts/macros/definitions/state-macros.js @@ -1,5 +1,6 @@ import { MacroRegistry, MacroCategory } from '../engine/MacroRegistry.js'; import { eventSource, event_types } from '../../events.js'; +import { findExtension } from '/scripts/extensions.js'; let lastGenerationTypeValue = ''; let lastGenerationTypeTrackingInitialized = false; @@ -37,4 +38,20 @@ export function registerStateMacros() { returns: 'Type of the last queued generation request.', handler: () => lastGenerationTypeValue, }); + + // Macro that checks if an extension is enabled + MacroRegistry.registerMacro('hasExtension', { + category: MacroCategory.STATE, + unnamedArgs: [{ + name: 'extensionName', + type: 'string', + description: 'The name of the extension to check', + }], + description: 'Checks if a specific extension is enabled. If the extension does not exist, returns false.', + returns: 'true if the extension is enabled, false otherwise.', + handler: ({ unnamedArgs: [extensionName] }) => { + const extension = findExtension(extensionName); + return String(extension?.enabled ?? false); + }, + }); }