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 @@
1import { disableExtension, enableExtension, extension_settings, extensionNames } from './extensions.js';1import { disableExtension, enableExtension, extensionNames, findExtension } from './extensions.js';
2import { SlashCommand } from './slash-commands/SlashCommand.js';2import { SlashCommand } from './slash-commands/SlashCommand.js';
3import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';3import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
4import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';4import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
5import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';5import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
6import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';6import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
7import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';7import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
8import { equalsIgnoreCaseAndAccents, isFalseBoolean, isTrueBoolean } from './utils.js';8import { isFalseBoolean, isTrueBoolean } from './utils.js';
99
10/**10/**
11 * @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension11 * @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension
@@ -22,30 +22,28 @@ function getExtensionActionCallback(action) {
22 }22 }
2323
24 const reload = !isFalseBoolean(args?.reload?.toString());24 const reload = !isFalseBoolean(args?.reload?.toString());
25 const internalExtensionName = findExtension(extensionName);25 const extension = findExtension(extensionName);
26 if (!internalExtensionName) {26 if (!extension) {
27 toastr.warning(`Extension ${extensionName} does not exist.`);27 toastr.warning(`Extension ${extensionName} does not exist.`);
28 return '';28 return '';
29 }29 }
3030
31 const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);31 if (action === 'enable' && extension.enabled) {
3232 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;
36 }34 }
3735
38 if (action === 'disable' && !isEnabled) {36 if (action === 'disable' && !extension.enabled) {
39 toastr.info(`Extension ${extensionName} is already disabled.`);37 toastr.info(`Extension ${extension.name} is already disabled.`);
40 return internalExtensionName;38 return extension.name;
41 }39 }
4240
43 if (action === 'toggle') {41 if (action === 'toggle') {
44 action = isEnabled ? 'disable' : 'enable';42 action = extension.enabled ? 'disable' : 'enable';
45 }43 }
4644
47 if (reload) {45 if (reload) {
48 toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extensionName} and reloading...`);46 toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extension.name} and reloading...`);
4947
50 // Clear input, so it doesn't stay because the command didn't "finish",48 // Clear input, so it doesn't stay because the command didn't "finish",
51 // and wait for a bit to both show the toast and let the clear bubble through.49 // and wait for a bit to both show the toast and let the clear bubble through.
@@ -54,36 +52,24 @@ function getExtensionActionCallback(action) {
54 }52 }
5553
56 if (action === 'enable') {54 if (action === 'enable') {
57 await enableExtension(internalExtensionName, reload);55 await enableExtension(extension.name, reload);
58 } else {56 } else {
59 await disableExtension(internalExtensionName, reload);57 await disableExtension(extension.name, reload);
60 }58 }
6159
62 toastr.success(`Extension ${extensionName} ${action}d.`);60 toastr.success(`Extension ${extension.name} ${action}d.`);
6361
6462
65 console.info(`Extension ${action}ed: ${extensionName}`);63 console.info(`Extension ${action}ed: ${extension.name}`);
66 if (!reload) {64 if (!reload) {
67 console.info('Reload not requested, so page needs to be reloaded manually for changes to take effect.');65 console.info('Reload not requested, so page needs to be reloaded manually for changes to take effect.');
68 }66 }
6967
70 return internalExtensionName;68 return extension.name;
71 };69 };
72}70}
7371
74/**72/**
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 */
80function findExtension(name) {
81 return extensionNames.find(extName => {
82 return equalsIgnoreCaseAndAccents(extName, name) || equalsIgnoreCaseAndAccents(extName, `third-party/${name}`);
83 });
84}
85
86/**
87 * Provides an array of SlashCommandEnumValue objects based on the extension names.73 * Provides an array of SlashCommandEnumValue objects based on the extension names.
88 * Each object contains the name of the extension and a description indicating if it is a third-party extension.74 * Each object contains the name of the extension and a description indicating if it is a third-party extension.
89 *75 *
@@ -244,14 +230,13 @@ export function registerExtensionSlashCommands() {
244 name: 'extension-state',230 name: 'extension-state',
245 callback: async (_, extensionName) => {231 callback: async (_, extensionName) => {
246 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');232 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
247 const internalExtensionName = findExtension(extensionName);233 const extension = findExtension(extensionName);
248 if (!internalExtensionName) {234 if (!extension) {
249 toastr.warning(`Extension ${extensionName} does not exist.`);235 toastr.warning(`Extension ${extensionName} does not exist.`);
250 return '';236 return '';
251 }237 }
252238
253 const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);239 return String(extension.enabled);
254 return String(isEnabled);
255 },240 },
256 returns: 'The state of the extension, whether it is enabled.',241 returns: 'The state of the extension, whether it is enabled.',
257 unnamedArgumentList: [242 unnamedArgumentList: [
@@ -282,8 +267,8 @@ export function registerExtensionSlashCommands() {
282 aliases: ['extension-installed'],267 aliases: ['extension-installed'],
283 callback: async (_, extensionName) => {268 callback: async (_, extensionName) => {
284 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');269 if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
285 const exists = findExtension(extensionName) !== undefined;270 const extension = findExtension(extensionName);
286 return exists ? 'true' : 'false';271 return extension !== null ? 'true' : 'false';
287 },272 },
288 returns: 'Whether the extension exists and is installed.',273 returns: 'Whether the extension exists and is installed.',
289 unnamedArgumentList: [274 unnamedArgumentList: [
public/scripts/extensions.js+16 -1
@@ -4,7 +4,7 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque
4import { showLoader } from './loader.js';4import { showLoader } from './loader.js';
5import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';5import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
6import { renderTemplate, renderTemplateAsync } from './templates.js';6import { renderTemplate, renderTemplateAsync } from './templates.js';
7import { delay, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js';7import { delay, equalsIgnoreCaseAndAccents, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js';
8import { getContext } from './st-context.js';8import { getContext } from './st-context.js';
9import { isAdmin } from './user.js';9import { isAdmin } from './user.js';
10import { addLocaleData, getCurrentLocale, t } from './i18n.js';10import { addLocaleData, getCurrentLocale, t } from './i18n.js';
@@ -332,6 +332,21 @@ export async function disableExtension(name, reload = true) {
332}332}
333333
334/**334/**
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 */
340export 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/**
335 * Loads manifest.json files for extensions.350 * Loads manifest.json files for extensions.
336 * @param {string[]} names Array of extension names351 * @param {string[]} names Array of extension names
337 * @returns {Promise<Record<string, object>>} Object with extension names as keys and their manifests as values352 * @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 @@
1import { MacroRegistry, MacroCategory } from '../engine/MacroRegistry.js';1import { MacroRegistry, MacroCategory } from '../engine/MacroRegistry.js';
2import { eventSource, event_types } from '../../events.js';2import { eventSource, event_types } from '../../events.js';
3import { findExtension } from '/scripts/extensions.js';
34
4let lastGenerationTypeValue = '';5let lastGenerationTypeValue = '';
5let lastGenerationTypeTrackingInitialized = false;6let lastGenerationTypeTrackingInitialized = false;
@@ -37,4 +38,20 @@ export function registerStateMacros() {
37 returns: 'Type of the last queued generation request.',38 returns: 'Type of the last queued generation request.',
38 handler: () => lastGenerationTypeValue,39 handler: () => lastGenerationTypeValue,
39 });40 });
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 });
40}57}