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

cd0627bfec3598ba0b7a8304145e3a746aad75e1

Wolfsblvt <wolfsblvt@gmail.com>

Signed
3 files changed, +55 -38Ignore whitespace
public/scripts/extensions-slashcommands.js+22 -37
@@ -1,11 +1,11 @@
11import { disableExtension, enableExtension, extension_settingsextensionNames, extensionNamesfindExtension } from './extensions.js';
22import { SlashCommand } from './slash-commands/SlashCommand.js';
33import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
44import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
55import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
66import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
77import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
88import { equalsIgnoreCaseAndAccents, isFalseBoolean, isTrueBoolean } from './utils.js';
99
1010/**
1111 * @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension
@@ -22,30 +22,28 @@ function getExtensionActionCallback(action) {
2222 }
2323
2424 const reload = !isFalseBoolean(args?.reload?.toString());
2525 const internalExtensionNameextension = findExtension(extensionName);
2626 if (!internalExtensionNameextension) {
2727 toastr.warning(`Extension ${extensionName} does not exist.`);
2828 return '';
2929 }
3030
31- const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
31+ if (action === 'enable' && extension.enabled) {
32-
32+ toastr.info(`Extension ${extension.name} is already enabled.`);
33- if (action === 'enable' && isEnabled) {
33+ return extension.name;
34- toastr.info(`Extension ${extensionName} is already enabled.`);
35- return internalExtensionName;
3634 }
3735
3836 if (action === 'disable' && !isEnabledextension.enabled) {
3937 toastr.info(`Extension ${extensionNameextension.name} is already disabled.`);
4038 return internalExtensionNameextension.name;
4139 }
4240
4341 if (action === 'toggle') {
4442 action = isEnabledextension.enabled ? 'disable' : 'enable';
4543 }
4644
4745 if (reload) {
4846 toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extensionNameextension.name} and reloading...`);
4947
5048 // Clear input, so it doesn't stay because the command didn't "finish",
5149 // and wait for a bit to both show the toast and let the clear bubble through.
@@ -54,36 +52,24 @@ function getExtensionActionCallback(action) {
5452 }
5553
5654 if (action === 'enable') {
5755 await enableExtension(internalExtensionNameextension.name, reload);
5856 } else {
5957 await disableExtension(internalExtensionNameextension.name, reload);
6058 }
6159
6260 toastr.success(`Extension ${extensionNameextension.name} ${action}d.`);
6361
6462
6563 console.info(`Extension ${action}ed: ${extensionNameextension.name}`);
6664 if (!reload) {
6765 console.info('Reload not requested, so page needs to be reloaded manually for changes to take effect.');
6866 }
6967
7068 return internalExtensionNameextension.name;
7169 };
7270}
7371
7472/**
75- * Finds an extension by name, allowing omission of the "third-party/" prefix.
76- *
77- * @param {string} name - The name of the extension to find
78- * @returns {string?} - The matched extension name or undefined if not found
79- */
80-function findExtension(name) {
81- return extensionNames.find(extName => {
82- return equalsIgnoreCaseAndAccents(extName, name) || equalsIgnoreCaseAndAccents(extName, `third-party/${name}`);
83- });
84-}
85-
86-/**
8773 * Provides an array of SlashCommandEnumValue objects based on the extension names.
8874 * Each object contains the name of the extension and a description indicating if it is a third-party extension.
8975 *
@@ -244,14 +230,13 @@ export function registerExtensionSlashCommands() {
244230 name: 'extension-state',
245231 callback: async (_, extensionName) => {
246232 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
247233 const internalExtensionNameextension = findExtension(extensionName);
248234 if (!internalExtensionNameextension) {
249235 toastr.warning(`Extension ${extensionName} does not exist.`);
250236 return '';
251237 }
252238
253- const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
239+ return String(extension.enabled);
254- return String(isEnabled);
255240 },
256241 returns: 'The state of the extension, whether it is enabled.',
257242 unnamedArgumentList: [
@@ -282,8 +267,8 @@ export function registerExtensionSlashCommands() {
282267 aliases: ['extension-installed'],
283268 callback: async (_, extensionName) => {
284269 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
285270 const existsextension = findExtension(extensionName) !== undefined;
286271 return existsextension !== null ? 'true' : 'false';
287272 },
288273 returns: 'Whether the extension exists and is installed.',
289274 unnamedArgumentList: [
public/scripts/extensions.js+16 -1
@@ -4,7 +4,7 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque
44import { showLoader } from './loader.js';
55import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
66import { renderTemplate, renderTemplateAsync } from './templates.js';
77import { delay, equalsIgnoreCaseAndAccents, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js';
88import { getContext } from './st-context.js';
99import { isAdmin } from './user.js';
1010import { addLocaleData, getCurrentLocale, t } from './i18n.js';
@@ -332,6 +332,21 @@ export async function disableExtension(name, reload = true) {
332332}
333333
334334/**
335+ * Finds an extension by name, allowing omission of the "third-party/" prefix.
336+ *
337+ * @param {string} name - The name of the extension to find
338+ * @returns {{name: string, enabled: boolean}|null} Object with name and enabled properties, or null if not found
339+ */
340+export function findExtension(name) {
341+ const internalExtensionName = extensionNames.find(extName => {
342+ return equalsIgnoreCaseAndAccents(extName, name) || equalsIgnoreCaseAndAccents(extName, `third-party/${name}`);
343+ });
344+ if (!internalExtensionName) return null;
345+ const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
346+ return { name: internalExtensionName, enabled: isEnabled };
347+}
348+
349+/**
335350 * Loads manifest.json files for extensions.
336351 * @param {string[]} names Array of extension names
337352 * @returns {Promise<Record<string, object>>} Object with extension names as keys and their manifests as values
public/scripts/macros/definitions/state-macros.js+17 -0
@@ -1,5 +1,6 @@
11import { MacroRegistry, MacroCategory } from '../engine/MacroRegistry.js';
22import { eventSource, event_types } from '../../events.js';
3+import { findExtension } from '/scripts/extensions.js';
34
45let lastGenerationTypeValue = '';
56let lastGenerationTypeTrackingInitialized = false;
@@ -37,4 +38,20 @@ export function registerStateMacros() {
3738 returns: 'Type of the last queued generation request.',
3839 handler: () => lastGenerationTypeValue,
3940 });
41+
42+ // Macro that checks if an extension is enabled
43+ MacroRegistry.registerMacro('hasExtension', {
44+ category: MacroCategory.STATE,
45+ unnamedArgs: [{
46+ name: 'extensionName',
47+ type: 'string',
48+ description: 'The name of the extension to check',
49+ }],
50+ description: 'Checks if a specific extension is enabled. If the extension does not exist, returns false.',
51+ returns: 'true if the extension is enabled, false otherwise.',
52+ handler: ({ unnamedArgs: [extensionName] }) => {
53+ const extension = findExtension(extensionName);
54+ return String(extension?.enabled ?? false);
55+ },
56+ });
4057}