STscript improvements (see below)

/abort command, {{pipe}} macro in named args, subcommand batch escaping with backslash, string literals and rules for /if, else clause for /if
This commit is contained in:
Cohee
2023-11-24 00:18:07 +02:00
parent 3594c4aac7
commit c50ed4bf6a
2 changed files with 112 additions and 47 deletions

View File

@ -163,12 +163,18 @@ parser.addCommand('echo', echoCallback, [], '<span class="monospace">(text)</spa
parser.addCommand('gen', generateCallback, [], '<span class="monospace">(prompt)</span> generates text using the provided prompt and passes it to the next command through the pipe.', true, true);
parser.addCommand('genraw', generateRawCallback, [], '<span class="monospace">(prompt)</span> generates text using the provided prompt and passes it to the next command through the pipe. Does not include chat history or character card.', true, true);
parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '<span class="monospace">(text)</span> adds a swipe to the last chat message.', true, true);
parser.addCommand('abort', abortCallback, [], ' aborts the slash command batch execution', true, true);
registerVariableCommands();
const NARRATOR_NAME_KEY = 'narrator_name';
const NARRATOR_NAME_DEFAULT = 'System';
export const COMMENT_NAME_DEFAULT = 'Note';
function abortCallback() {
$('#send_textarea').val('');
throw new Error('/abort command executed');
}
async function generateRawCallback(_, arg) {
if (!arg) {
console.warn('WARN: No argument provided for /genraw command');
@ -981,16 +987,29 @@ function setBackgroundCallback(_, bg) {
/**
* Executes slash commands in the provided text
* @param {string} text Slash command text
* @param {boolean} unescape Whether to unescape the batch separator
* @returns {Promise<{interrupt: boolean, newText: string, pipe: string} | boolean>}
*/
async function executeSlashCommands(text) {
async function executeSlashCommands(text, unescape = false) {
if (!text) {
return false;
}
// Unescape the pipe character
if (unescape) {
text = text.replace(/\\\|/g, '|');
}
// Hack to allow multi-line slash commands
// All slash command messages should begin with a slash
const lines = text.split('|').map(line => line.trim());
const placeholder = '\u200B'; // Use a zero-width space as a placeholder
const chars = text.split('');
for (let i = 1; i < chars.length; i++) {
if (chars[i] === '|' && chars[i - 1] !== '\\') {
chars[i] = placeholder;
}
}
const lines = chars.join('').split(placeholder).map(line => line.trim());
const linesToRemove = [];
let interrupt = false;
@ -1016,7 +1035,15 @@ async function executeSlashCommands(text) {
console.debug('Slash command executing:', result);
let unnamedArg = result.value || pipeResult;
if (typeof unnamedArg === 'string' && /{{pipe}}/i.test(unnamedArg)) {
if (pipeResult && typeof result.args === 'object') {
for (const [key, value] of Object.entries(result.args)) {
if (typeof value === 'string' && /{{pipe}}/i.test(value)) {
result.args[key] = value.replace(/{{pipe}}/i, pipeResult);
}
}
}
if (pipeResult && typeof unnamedArg === 'string' && /{{pipe}}/i.test(unnamedArg)) {
unnamedArg = unnamedArg.replace(/{{pipe}}/i, pipeResult);
}

View File

@ -3,6 +3,10 @@ import { extension_settings } from "./extensions.js";
import { executeSlashCommands, registerSlashCommand } from "./slash-commands.js";
function getLocalVariable(name) {
if (!chat_metadata.variables) {
chat_metadata.variables = {};
}
const localVariable = chat_metadata?.variables[name];
return localVariable || '';
@ -125,25 +129,48 @@ function listVariablesCallback() {
}
async function ifCallback(args, command) {
const a = getLocalVariable(args.a) || getGlobalVariable(args.a) || Number(args.a);
const b = getLocalVariable(args.b) || getGlobalVariable(args.b) || Number(args.b);
// Resultion order: numeric literal, local variable, global variable, string literal
const a = isNaN(Number(args.a)) ? (getLocalVariable(args.a) || getGlobalVariable(args.a) || args.a || '') : Number(args.a);
const b = isNaN(Number(args.b)) ? (getLocalVariable(args.b) || getGlobalVariable(args.b) || args.b || '') : Number(args.b);
const rule = args.rule;
if (!a || !b || !rule) {
if (!rule) {
toastr.warning('Both operands and the rule must be specified for the /if command.', 'Invalid /if command');
return '';
}
const aNumber = Number(a);
const bNumber = Number(b);
if (isNaN(aNumber) || isNaN(bNumber)) {
toastr.warning('Both operands must be numbers for the /if command.', 'Invalid /if command');
if ((typeof a === 'number' && isNaN(a)) || (typeof a === 'string' && a === '')) {
toastr.warning('The first operand must be a number, string or a variable name for the /if command.', 'Invalid /if command');
return '';
}
let result = false;
if (typeof a === 'string') {
const aString = String(a).toLowerCase();
const bString = String(b).toLowerCase();
switch (rule) {
case 'in':
result = aString.includes(bString);
break;
case 'nin':
result = !aString.includes(bString);
break;
case 'eq':
result = aString === bString;
break;
case 'neq':
result = aString !== bString;
break;
default:
toastr.warning('Unknown rule for the /if command for type string.', 'Invalid /if command');
return '';
}
} else if (typeof a === 'number') {
const aNumber = Number(a);
const bNumber = Number(b);
switch (rule) {
case 'gt':
result = aNumber > bNumber;
@ -160,12 +187,25 @@ async function ifCallback(args, command) {
case 'eq':
result = aNumber === bNumber;
break;
case 'neq':
result = aNumber !== bNumber;
break;
default:
toastr.warning('Unknown rule for the /if command.', 'Invalid /if command');
toastr.warning('Unknown rule for the /if command for type number.', 'Invalid /if command');
return '';
}
}
if (result && command) {
return await executeSubCommands(command);
} else if (!result && args.else && typeof args.else === 'string' && args.else !== '') {
return await executeSubCommands(args.else);
}
return '';
}
async function executeSubCommands(command) {
if (command.startsWith('"')) {
command = command.slice(1);
}
@ -174,16 +214,14 @@ async function ifCallback(args, command) {
command = command.slice(0, -1);
}
const result = await executeSlashCommands(command);
const unescape = true;
const result = await executeSlashCommands(command, unescape);
if (!result || typeof result !== 'object') {
return '';
}
return result?.pipe || '';
}
return '';
}
export function registerVariableCommands() {
@ -194,5 +232,5 @@ export function registerVariableCommands() {
registerSlashCommand('setglobalvar', (args, value) => setGlobalVariable(args.key || args.name, value), [], '<span class="monospace">key=varname (value)</span> set a global variable value and pass it down the pipe, e.g. <tt>/setglobalvar key=color green</tt>', true, true);
registerSlashCommand('getglobalvar', (_, value) => getGlobalVariable(value), [], '<span class="monospace">(key)</span> get a global variable value and pass it down the pipe, e.g. <tt>/getglobalvar height</tt>', true, true);
registerSlashCommand('addglobalvar', (args, value) => addGlobalVariable(args.key || args.name, value), [], '<span class="monospace">key=varname (increment)</span> add a value to a global variable and pass the result down the pipe, e.g. <tt>/addglobalvar score 10</tt>', true, true);
registerSlashCommand('if', ifCallback, [], '<span class="monospace">a=varname1 b=varname2 rule=comparison "(command)"</span> compare the value of variable "a" with the value of variable "b", and if the condition yields true, then execute any valid slash command enclosed in quotes and pass the result of the command execution down the pipe. Numeric values for "a" and "b" supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b e.g. <tt>/if a=score a=10 rule=gte "/speak You win"</tt> triggers a /speak command if the value of "score" is greater or equals 10.', true, true);
registerSlashCommand('if', ifCallback, [], '<span class="monospace">a=varname1 b=varname2 rule=comparison else="(alt.command)" "(command)"</span> compare the value of variable "a" with the value of variable "b", and if the condition yields true, then execute any valid slash command enclosed in quotes and pass the result of the command execution down the pipe. Numeric values and string literals for "a" and "b" supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b, neq => a != b, in (strings) => a includes b, nin (strings) => a not includes b, e.g. <tt>/if a=score a=10 rule=gte "/speak You win"</tt> triggers a /speak command if the value of "score" is greater or equals 10.', true, true);
}