Macros 2.0 [Macros] - Add {{hasExtension}} macro, and refactor extension lookup logic (#4948)

* 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
This commit is contained in:
Wolfsblvt
2026-01-04 19:37:27 +01:00
committed by GitHub
parent e6ce84d726
commit cd0627bfec
3 changed files with 55 additions and 38 deletions
+22 -37
View File
@@ -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: [
+16 -1
View File
@@ -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
@@ -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);
},
});
}