mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
basics for new parser
This commit is contained in:
@ -1,4 +1,5 @@
|
|||||||
import { callPopup } from '../../../../script.js';
|
import { callPopup } from '../../../../script.js';
|
||||||
|
import { setNewSlashCommandAutoComplete } from '../../../slash-commands.js';
|
||||||
import { getSortableDelay } from '../../../utils.js';
|
import { getSortableDelay } from '../../../utils.js';
|
||||||
import { log, warn } from '../index.js';
|
import { log, warn } from '../index.js';
|
||||||
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
|
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
|
||||||
@ -213,6 +214,7 @@ export class QuickReply {
|
|||||||
message.addEventListener('input', () => {
|
message.addEventListener('input', () => {
|
||||||
this.updateMessage(message.value);
|
this.updateMessage(message.value);
|
||||||
});
|
});
|
||||||
|
setNewSlashCommandAutoComplete(message, true);
|
||||||
//TODO move tab support for textarea into its own helper(?) and use for both this and .editor_maximize
|
//TODO move tab support for textarea into its own helper(?) and use for both this and .editor_maximize
|
||||||
message.addEventListener('keydown', (evt) => {
|
message.addEventListener('keydown', (evt) => {
|
||||||
if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
|
if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
|
||||||
|
@ -33,6 +33,9 @@ import {
|
|||||||
system_message_types,
|
system_message_types,
|
||||||
this_chid,
|
this_chid,
|
||||||
} from '../script.js';
|
} from '../script.js';
|
||||||
|
import { SlashCommandParser as NewSlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||||
|
import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js';
|
||||||
|
import { SlashCommandExecutor } from './slash-commands/SlashCommandExecutor.js';
|
||||||
import { getMessageTimeStamp } from './RossAscends-mods.js';
|
import { getMessageTimeStamp } from './RossAscends-mods.js';
|
||||||
import { hideChatMessage, unhideChatMessage } from './chats.js';
|
import { hideChatMessage, unhideChatMessage } from './chats.js';
|
||||||
import { getContext, saveMetadataDebounced } from './extensions.js';
|
import { getContext, saveMetadataDebounced } from './extensions.js';
|
||||||
@ -43,7 +46,7 @@ import { autoSelectPersona } from './personas.js';
|
|||||||
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
|
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
|
||||||
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
|
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
|
||||||
import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCount } from './tokenizers.js';
|
import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCount } from './tokenizers.js';
|
||||||
import { delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
|
import { debounce, delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
|
||||||
import { registerVariableCommands, resolveVariable } from './variables.js';
|
import { registerVariableCommands, resolveVariable } from './variables.js';
|
||||||
export {
|
export {
|
||||||
executeSlashCommands, getSlashCommandsHelp, registerSlashCommand,
|
executeSlashCommands, getSlashCommandsHelp, registerSlashCommand,
|
||||||
@ -181,7 +184,7 @@ class SlashCommandParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const parser = new SlashCommandParser();
|
export const parser = new NewSlashCommandParser();
|
||||||
const registerSlashCommand = parser.addCommand.bind(parser);
|
const registerSlashCommand = parser.addCommand.bind(parser);
|
||||||
const getSlashCommandsHelp = parser.getHelpString.bind(parser);
|
const getSlashCommandsHelp = parser.getHelpString.bind(parser);
|
||||||
|
|
||||||
@ -1684,11 +1687,36 @@ function modelCallback(_, model) {
|
|||||||
* @param {boolean} unescape Whether to unescape the batch separator
|
* @param {boolean} unescape Whether to unescape the batch separator
|
||||||
* @returns {Promise<{interrupt: boolean, newText: string, pipe: string} | boolean>}
|
* @returns {Promise<{interrupt: boolean, newText: string, pipe: string} | boolean>}
|
||||||
*/
|
*/
|
||||||
async function executeSlashCommands(text, unescape = false) {
|
async function executeSlashCommands(text, unescape = false, handleParserErrors = true, scope = null) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let closure;
|
||||||
|
try {
|
||||||
|
closure = parser.parse(text);
|
||||||
|
closure.scope.parent = scope;
|
||||||
|
} catch (e) {
|
||||||
|
if (handleParserErrors && e instanceof SlashCommandParserError) {
|
||||||
|
/**@type {SlashCommandParserError}*/
|
||||||
|
const ex = e;
|
||||||
|
const toast = `
|
||||||
|
<div>${ex.message}</div>
|
||||||
|
<div>Line: ${ex.line} Column: ${ex.column}</div>
|
||||||
|
<pre style="text-align:left;">${ex.hint}</pre>
|
||||||
|
`;
|
||||||
|
toastr.error(
|
||||||
|
toast,
|
||||||
|
'SlashCommandParserError',
|
||||||
|
{ escapeHtml:false, timeOut: 10000, onclick:()=>callPopup(toast, 'text') },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await closure.execute();
|
||||||
|
|
||||||
// Unescape the pipe character and macro braces
|
// Unescape the pipe character and macro braces
|
||||||
if (unescape) {
|
if (unescape) {
|
||||||
text = text.replace(/\\\|/g, '|');
|
text = text.replace(/\\\|/g, '|');
|
||||||
@ -1782,40 +1810,321 @@ async function executeSlashCommands(text, unescape = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setSlashCommandAutocomplete(textarea) {
|
function setSlashCommandAutocomplete(textarea) {
|
||||||
|
/**@type {Number}*/
|
||||||
|
let width;
|
||||||
|
/**@type {HTMLTextAreaElement}*/
|
||||||
|
let element;
|
||||||
|
/**@type {String}*/
|
||||||
|
let text;
|
||||||
|
/**@type {SlashCommandExecutor}*/
|
||||||
|
let executor;
|
||||||
|
/**@type {Boolean}*/
|
||||||
|
let isReplacable;
|
||||||
|
textarea[0].addEventListener('keyup', ()=>isReplacable ? null : textarea.autocomplete('search'));
|
||||||
textarea.autocomplete({
|
textarea.autocomplete({
|
||||||
source: (input, output) => {
|
source: (input, output) => {
|
||||||
// Only show for slash commands and if there's no space
|
// Only show for slash commands
|
||||||
if (!input.term.startsWith('/') || input.term.includes(' ')) {
|
if (!input.term.startsWith('/')) {
|
||||||
output([]);
|
output([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const slashCommand = input.term.toLowerCase().substring(1); // Remove the slash
|
element = textarea[0];
|
||||||
const result = Object
|
text = input.term;
|
||||||
.keys(parser.helpStrings) // Get all slash commands
|
executor = parser.getCommandAt(text, element.selectionStart);
|
||||||
|
const slashCommand = executor?.name?.toLowerCase() ?? '';
|
||||||
|
isReplacable = !executor ? true : element.selectionStart + 1 == executor.start + executor.name.length;
|
||||||
|
|
||||||
|
window.parser = parser;
|
||||||
|
const helpStrings = Object
|
||||||
|
.keys(parser.commands) // Get all slash commands
|
||||||
.filter(x => x.startsWith(slashCommand)) // Filter by the input
|
.filter(x => x.startsWith(slashCommand)) // Filter by the input
|
||||||
.sort((a, b) => a.localeCompare(b)) // Sort alphabetically
|
.sort((a, b) => a.localeCompare(b)) // Sort alphabetically
|
||||||
// .slice(0, 20) // Limit to 20 results
|
;
|
||||||
.map(x => ({ label: parser.helpStrings[x], value: `/${x} ` })); // Map to the help string
|
const result = helpStrings
|
||||||
|
.filter((it,idx)=>helpStrings.indexOf(it) == idx) // remove duplicates
|
||||||
|
.map(x => ({ label: parser.commands[x].helpStringFormatted, value: `/${x} ` })) // Map to the help string
|
||||||
|
;
|
||||||
|
|
||||||
|
// add notice if no match found
|
||||||
|
if (result.length == 0) {
|
||||||
|
result.push({ label:`No matching commands for "/${slashCommand}"`, value:'' });
|
||||||
|
}
|
||||||
|
console.log(result);
|
||||||
|
|
||||||
|
// determine textarea width *once* before generating output
|
||||||
|
width = element.getBoundingClientRect().width;
|
||||||
output(result); // Return the results
|
output(result); // Return the results
|
||||||
},
|
},
|
||||||
select: (e, u) => {
|
select: (e, u) => {
|
||||||
// unfocus the input
|
e.preventDefault();
|
||||||
$(e.target).val(u.item.value);
|
// only update value if no space after command name
|
||||||
|
if (isReplacable) {
|
||||||
|
element.value = `${text.slice(0, executor.start - 2)}${u.item.value}${text.slice(executor.start + executor.name.length)}`;
|
||||||
|
element.selectionStart = executor.start + u.item.value.length - 2;
|
||||||
|
element.selectionEnd = element.selectionStart;
|
||||||
|
} else {
|
||||||
|
console.log('[AUTOCOMPLETE]', '[SELECT]', {e, u});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focus: (e, u) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// only update value if no space after command name
|
||||||
|
if (isReplacable) {
|
||||||
|
element.value = `${text.slice(0, executor.start - 2)}${u.item.value}${text.slice(executor.start + executor.name.length)}`;
|
||||||
|
element.selectionStart = executor.start + u.item.value.length - 2;
|
||||||
|
element.selectionEnd = element.selectionStart;
|
||||||
|
} else {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowUp': {
|
||||||
|
const line = text.slice(0, element.selectionStart).replace(/[^\n]/g, '').length;
|
||||||
|
if (line == 0) {
|
||||||
|
element.selectionStart = 0;
|
||||||
|
} else {
|
||||||
|
const lines = text.slice(0, element.selectionStart).split('\n');
|
||||||
|
console.log(lines.slice(-2)[0]);
|
||||||
|
element.selectionStart -= Math.max(lines.slice(-1)[0].length + 1, lines.slice(-2)[0].length + 1);
|
||||||
|
}
|
||||||
|
element.selectionEnd = element.selectionStart;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ArrowDown': {
|
||||||
|
const line = text.slice(0, element.selectionStart).replace(/[^\n]/g, '').length;
|
||||||
|
const lines = text.split('\n');
|
||||||
|
if (line + 1 == lines.length) {
|
||||||
|
element.selectionStart = text.length;
|
||||||
|
} else {
|
||||||
|
element.selectionStart += lines[line].length + 1;
|
||||||
|
}
|
||||||
|
element.selectionEnd = element.selectionStart;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
position: { my: 'left bottom', at: 'left top', collision: 'none' },
|
position: { my: 'left bottom', at: 'left top', collision: 'none' },
|
||||||
});
|
});
|
||||||
|
|
||||||
textarea.autocomplete('instance')._renderItem = function (ul, item) {
|
textarea.autocomplete('instance')._renderItem = function (ul, item) {
|
||||||
const width = $(textarea).innerWidth();
|
const li = document.createElement('li'); {
|
||||||
const content = $('<div></div>').html(item.label);
|
li.style.width = `${width}px`;
|
||||||
return $('<li>').width(width).append(content).appendTo(ul);
|
const div = document.createElement('div'); {
|
||||||
|
div.innerHTML = item.label;
|
||||||
|
li.append(div);
|
||||||
|
}
|
||||||
|
ul.append(li);
|
||||||
|
}
|
||||||
|
return $(li);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {HTMLTextAreaElement} textarea
|
||||||
|
*/
|
||||||
|
export function setNewSlashCommandAutoComplete(textarea, isFloating = false) {
|
||||||
|
const dom = document.createElement('ul'); {
|
||||||
|
dom.classList.add('slashCommandAutoComplete');
|
||||||
|
}
|
||||||
|
let isReplacable = false;
|
||||||
|
let result = [];
|
||||||
|
let selectedItem = null;
|
||||||
|
let isActive = false;
|
||||||
|
let text;
|
||||||
|
let executor;
|
||||||
|
let clone;
|
||||||
|
let hasFocus = false;
|
||||||
|
const hide = () => {
|
||||||
|
dom?.remove();
|
||||||
|
isActive = false;
|
||||||
|
};
|
||||||
|
const show = () => {
|
||||||
|
text = textarea.value;
|
||||||
|
// only show with textarea in focus
|
||||||
|
if (document.activeElement != textarea) return hide();
|
||||||
|
// only show for slash commands
|
||||||
|
if (text[0] != '/') return hide();
|
||||||
|
|
||||||
|
executor = parser.getCommandAt(text, textarea.selectionStart);
|
||||||
|
const slashCommand = executor?.name?.toLowerCase() ?? '';
|
||||||
|
isReplacable = !executor ? true : textarea.selectionStart == executor.start - 2 + executor.name.length + 1;
|
||||||
|
|
||||||
|
// don't show if no executor found, i.e. cursor's area is not a command
|
||||||
|
if (!executor) return hide();
|
||||||
|
else {
|
||||||
|
const helpStrings = Object
|
||||||
|
.keys(parser.commands) // Get all slash commands
|
||||||
|
.filter(it => executor.name == '' || isReplacable ? it.toLowerCase().startsWith(slashCommand) : it.toLowerCase() == slashCommand) // Filter by the input
|
||||||
|
.sort((a, b) => a.localeCompare(b)) // Sort alphabetically
|
||||||
|
;
|
||||||
|
result = helpStrings
|
||||||
|
.filter((it,idx)=>[idx, -1].includes(helpStrings.indexOf(parser.commands[it].name.toLowerCase()))) // remove duplicates
|
||||||
|
.map(it => ({ label: parser.commands[it].helpStringFormatted, value: `/${it} `, li:null })) // Map to the help string
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add notice if no match found
|
||||||
|
if (result.length == 0) {
|
||||||
|
result.push({ label:`No matching commands for "/${slashCommand}"`, value:'', li:null });
|
||||||
|
} else if (result.length == 1 && result[0].value == `/${executor.name} `) {
|
||||||
|
isReplacable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dom.innerHTML = '';
|
||||||
|
for (const item of result) {
|
||||||
|
const li = document.createElement('li'); {
|
||||||
|
li.classList.add('item');
|
||||||
|
if (item == result[0]) {
|
||||||
|
li.classList.add('selected');
|
||||||
|
}
|
||||||
|
li.innerHTML = item.label;
|
||||||
|
li.addEventListener('click', ()=>{
|
||||||
|
selectedItem = item;
|
||||||
|
select();
|
||||||
|
});
|
||||||
|
item.li = li;
|
||||||
|
dom.append(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedItem = result[0];
|
||||||
|
if (isFloating) {
|
||||||
|
const location = getCursorPosition();
|
||||||
|
if (location.y <= window.innerHeight / 2) {
|
||||||
|
dom.style.top = `${location.bottom}px`;
|
||||||
|
dom.style.bottom = 'auto';
|
||||||
|
dom.style.left = `${location.left}px`;
|
||||||
|
dom.style.right = 'auto';
|
||||||
|
dom.style.maxWidth = `calc(99vw - ${location.left}px)`;
|
||||||
|
dom.style.maxHeight = `calc(99vh - ${location.bottom}px)`;
|
||||||
|
} else {
|
||||||
|
dom.style.top = 'auto';
|
||||||
|
dom.style.bottom = `calc(100vh - ${location.top}px)`;
|
||||||
|
dom.style.left = `${location.left}px`;
|
||||||
|
dom.style.right = 'auto';
|
||||||
|
dom.style.maxWidth = `calc(99vw - ${location.left}px)`;
|
||||||
|
dom.style.maxHeight = `calc(99vh - ${location.top}px)`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const rect = textarea.getBoundingClientRect();
|
||||||
|
dom.style.setProperty('--bottom', `${window.innerHeight - rect.top}px`);
|
||||||
|
dom.style.bottom = `${window.innerHeight - rect.top}px`;
|
||||||
|
dom.style.left = `${rect.left}px`;
|
||||||
|
dom.style.right = `${window.innerWidth - rect.right}px`;
|
||||||
|
}
|
||||||
|
document.body.append(dom);
|
||||||
|
isActive = true;
|
||||||
|
};
|
||||||
|
const getCursorPosition = () => {
|
||||||
|
const inputRect = textarea.getBoundingClientRect();
|
||||||
|
clone?.remove();
|
||||||
|
clone = document.createElement('div');
|
||||||
|
const style = window.getComputedStyle(textarea);
|
||||||
|
for (const key of style) {
|
||||||
|
clone.style[key] = style[key];
|
||||||
|
}
|
||||||
|
clone.style.height = `${inputRect.height}px`;
|
||||||
|
clone.style.left = `${inputRect.left}px`;
|
||||||
|
clone.style.top = `${inputRect.top}px`;
|
||||||
|
clone.style.position = 'fixed';
|
||||||
|
// clone.style.whiteSpace = 'pre-wrap';
|
||||||
|
clone.style.zIndex = '10000';
|
||||||
|
clone.style.visibility = 'hidden';
|
||||||
|
// clone.style.opacity = 0.5;
|
||||||
|
const text = textarea.value;
|
||||||
|
const before = text.slice(0, textarea.selectionStart);
|
||||||
|
clone.textContent = before;
|
||||||
|
const locator = document.createElement('span');
|
||||||
|
// locator.textContent = '.';
|
||||||
|
locator.textContent = text[textarea.selectionStart];
|
||||||
|
clone.append(locator);
|
||||||
|
clone.append(text.slice(textarea.selectionStart + 1));
|
||||||
|
document.body.append(clone);
|
||||||
|
clone.scrollTop = textarea.scrollTop;
|
||||||
|
const locatorRect = locator.getBoundingClientRect();
|
||||||
|
const location = {
|
||||||
|
left: locatorRect.left,
|
||||||
|
top: locatorRect.top,// - textarea.scrollTop,
|
||||||
|
bottom: locatorRect.bottom,// - textarea.scrollTop,
|
||||||
|
};
|
||||||
|
clone.remove();
|
||||||
|
return location;
|
||||||
|
};
|
||||||
|
const select = () => {
|
||||||
|
if (isReplacable) {
|
||||||
|
textarea.focus();
|
||||||
|
textarea.value = `${text.slice(0, executor.start - 2)}${selectedItem.value}${text.slice(executor.start - 2 + executor.name.length + 1)}`;
|
||||||
|
textarea.selectionStart = executor.start - 2 + selectedItem.value.length;
|
||||||
|
textarea.selectionEnd = textarea.selectionStart;
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const showAutoCompleteDebounced = debounce(()=>show(), 100);
|
||||||
|
textarea.addEventListener('input', ()=>showAutoCompleteDebounced());
|
||||||
|
textarea.addEventListener('click', ()=>showAutoCompleteDebounced());
|
||||||
|
textarea.addEventListener('keydown', (evt)=>{
|
||||||
|
if (isActive && isReplacable) {
|
||||||
|
switch (evt.key) {
|
||||||
|
case 'ArrowUp': {
|
||||||
|
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
const idx = result.indexOf(selectedItem);
|
||||||
|
let newIdx;
|
||||||
|
if (idx == 0) newIdx = result.length - 1;
|
||||||
|
else newIdx = idx - 1;
|
||||||
|
selectedItem.li.classList.remove('selected');
|
||||||
|
selectedItem = result[newIdx];
|
||||||
|
selectedItem.li.classList.add('selected');
|
||||||
|
const rect = selectedItem.li.getBoundingClientRect();
|
||||||
|
const rectParent = dom.getBoundingClientRect();
|
||||||
|
if (rect.top < rectParent.top || rect.bottom > rectParent.bottom ) {
|
||||||
|
selectedItem.li.scrollIntoView();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'ArrowDown': {
|
||||||
|
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
const idx = result.indexOf(selectedItem);
|
||||||
|
const newIdx = (idx + 1) % result.length;
|
||||||
|
selectedItem.li.classList.remove('selected');
|
||||||
|
selectedItem = result[newIdx];
|
||||||
|
selectedItem.li.classList.add('selected');
|
||||||
|
const rect = selectedItem.li.getBoundingClientRect();
|
||||||
|
const rectParent = dom.getBoundingClientRect();
|
||||||
|
if (rect.top < rectParent.top || rect.bottom > rectParent.bottom ) {
|
||||||
|
selectedItem.li.scrollIntoView();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'Tab': {
|
||||||
|
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopImmediatePropagation();
|
||||||
|
select();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isActive) {
|
||||||
|
switch (evt.key) {
|
||||||
|
case 'Escape': {
|
||||||
|
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showAutoCompleteDebounced();
|
||||||
|
});
|
||||||
|
textarea.addEventListener('blur', ()=>hide());
|
||||||
|
}
|
||||||
|
|
||||||
jQuery(function () {
|
jQuery(function () {
|
||||||
const textarea = $('#send_textarea');
|
const textarea = $('#send_textarea');
|
||||||
setSlashCommandAutocomplete(textarea);
|
// setSlashCommandAutocomplete(textarea);
|
||||||
|
setNewSlashCommandAutoComplete(document.querySelector('#send_textarea'));
|
||||||
});
|
});
|
||||||
|
9
public/scripts/slash-commands/SlashCommand.js
Normal file
9
public/scripts/slash-commands/SlashCommand.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export class SlashCommand {
|
||||||
|
/**@type {String}*/ name;
|
||||||
|
/**@type {Function}*/ callback;
|
||||||
|
/**@type {String}*/ helpString;
|
||||||
|
/**@type {String}*/ helpStringFormatted;
|
||||||
|
/**@type {Boolean}*/ interruptsGeneration;
|
||||||
|
/**@type {Boolean}*/ purgeFromMessage;
|
||||||
|
/**@type {String[]}*/ aliases;
|
||||||
|
}
|
123
public/scripts/slash-commands/SlashCommandClosure.js
Normal file
123
public/scripts/slash-commands/SlashCommandClosure.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { substituteParams } from '../../script.js';
|
||||||
|
import { SlashCommandClosureResult } from './SlashCommandClosureResult.js';
|
||||||
|
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
||||||
|
import { SlashCommandScope } from './SlashCommandScope.js';
|
||||||
|
|
||||||
|
export class SlashCommandClosure {
|
||||||
|
/**@type {SlashCommandScope}*/ scope;
|
||||||
|
/**@type {Boolean}*/ executeNow = false;
|
||||||
|
/**@type {SlashCommandExecutor[]}*/ executorList = [];
|
||||||
|
/**@type {String}*/ keptText;
|
||||||
|
|
||||||
|
constructor(parent) {
|
||||||
|
this.scope = new SlashCommandScope(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
substituteParams(text) {
|
||||||
|
text = substituteParams(text)
|
||||||
|
.replace(/{{pipe}}/g, this.scope.pipe)
|
||||||
|
.replace(/{{var::(\w+?)}}/g, (_, key)=>this.scope.getVariable(key))
|
||||||
|
;
|
||||||
|
for (const key of Object.keys(this.scope.macros)) {
|
||||||
|
text = text.replace(new RegExp(`{{${key}}}`), this.scope.macros[key]);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCopy() {
|
||||||
|
const closure = new SlashCommandClosure();
|
||||||
|
closure.scope = this.scope.getCopy();
|
||||||
|
closure.executeNow = this.executeNow;
|
||||||
|
closure.executorList = this.executorList;
|
||||||
|
closure.keptText = this.keptText;
|
||||||
|
return closure;
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute() {
|
||||||
|
const closure = this.getCopy();
|
||||||
|
return await closure.executeDirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeDirect() {
|
||||||
|
let interrupt = false;
|
||||||
|
|
||||||
|
for (const executor of this.executorList) {
|
||||||
|
interrupt = executor.command.interruptsGeneration;
|
||||||
|
let args = {
|
||||||
|
_scope: this.scope,
|
||||||
|
};
|
||||||
|
let value;
|
||||||
|
// substitute named arguments
|
||||||
|
for (const key of Object.keys(executor.args)) {
|
||||||
|
if (executor.args[key] instanceof SlashCommandClosure) {
|
||||||
|
/**@type {SlashCommandClosure}*/
|
||||||
|
const closure = executor.args[key];
|
||||||
|
closure.scope.parent = this.scope;
|
||||||
|
if (closure.executeNow) {
|
||||||
|
args[key] = (await closure.execute())?.pipe;
|
||||||
|
} else {
|
||||||
|
args[key] = closure;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args[key] = this.substituteParams(executor.args[key]);
|
||||||
|
}
|
||||||
|
// unescape named argument
|
||||||
|
if (typeof args[key] == 'string') {
|
||||||
|
args[key] = args[key]
|
||||||
|
?.replace(/\\\|/g, '|')
|
||||||
|
?.replace(/\\\{/g, '{')
|
||||||
|
?.replace(/\\\}/g, '}')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// substitute unnamed argument
|
||||||
|
if (executor.value === undefined) {
|
||||||
|
value = this.scope.pipe;
|
||||||
|
} else if (executor.value instanceof SlashCommandClosure) {
|
||||||
|
/**@type {SlashCommandClosure}*/
|
||||||
|
const closure = executor.value;
|
||||||
|
closure.scope.parent = this.scope;
|
||||||
|
if (closure.executeNow) {
|
||||||
|
value = (await closure.execute())?.pipe;
|
||||||
|
} else {
|
||||||
|
value = closure;
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(executor.value)) {
|
||||||
|
value = [];
|
||||||
|
for (let i = 0; i < executor.value.length; i++) {
|
||||||
|
let v = executor.value[i];
|
||||||
|
if (v instanceof SlashCommandClosure) {
|
||||||
|
/**@type {SlashCommandClosure}*/
|
||||||
|
const closure = v;
|
||||||
|
closure.scope.parent = this.scope;
|
||||||
|
if (closure.executeNow) {
|
||||||
|
v = (await closure.execute())?.pipe;
|
||||||
|
} else {
|
||||||
|
v = closure;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
v = this.substituteParams(v);
|
||||||
|
}
|
||||||
|
value[i] = v;
|
||||||
|
}
|
||||||
|
if (!value.find(it=>it instanceof SlashCommandClosure)) {
|
||||||
|
value = value.join(' ');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = this.substituteParams(executor.value);
|
||||||
|
}
|
||||||
|
// unescape unnamed argument
|
||||||
|
if (typeof value == 'string') {
|
||||||
|
value = value
|
||||||
|
?.replace(/\\\|/g, '|')
|
||||||
|
?.replace(/\\\{/g, '{')
|
||||||
|
?.replace(/\\\}/g, '}')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scope.pipe = await executor.command.callback(args, value);
|
||||||
|
}
|
||||||
|
return Object.assign(new SlashCommandClosureResult(), { interrupt, newText: this.keptText, pipe: this.scope.pipe });
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
export class SlashCommandClosureResult {
|
||||||
|
/**@type {Boolean}*/ interrupt = false;
|
||||||
|
/**@type {String}*/ newText = '';
|
||||||
|
/**@type {String}*/ pipe;
|
||||||
|
}
|
18
public/scripts/slash-commands/SlashCommandExecutor.js
Normal file
18
public/scripts/slash-commands/SlashCommandExecutor.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { SlashCommand } from './SlashCommand.js';
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||||
|
|
||||||
|
export class SlashCommandExecutor {
|
||||||
|
/**@type {Number}*/ start;
|
||||||
|
/**@type {Number}*/ end;
|
||||||
|
/**@type {String}*/ name = '';
|
||||||
|
/**@type {SlashCommand}*/ command;
|
||||||
|
// @ts-ignore
|
||||||
|
/**@type {Map<String,String|SlashCommandClosure>}*/ args = {};
|
||||||
|
/**@type {String|SlashCommandClosure|(String|SlashCommandClosure)[]}*/ value;
|
||||||
|
|
||||||
|
constructor(start) {
|
||||||
|
this.start = start;
|
||||||
|
}
|
||||||
|
}
|
266
public/scripts/slash-commands/SlashCommandParser.js
Normal file
266
public/scripts/slash-commands/SlashCommandParser.js
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
import { SlashCommand } from './SlashCommand.js';
|
||||||
|
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||||
|
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
||||||
|
import { SlashCommandParserError } from './SlashCommandParserError.js';
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { SlashCommandScope } from './SlashCommandScope.js';
|
||||||
|
|
||||||
|
export class SlashCommandParser {
|
||||||
|
// @ts-ignore
|
||||||
|
/**@type {Map<String, SlashCommand>}*/ commands = {};
|
||||||
|
// @ts-ignore
|
||||||
|
/**@type {Map<String, String>}*/ helpStrings = {};
|
||||||
|
/**@type {String}*/ text;
|
||||||
|
/**@type {String}*/ keptText;
|
||||||
|
/**@type {Number}*/ index;
|
||||||
|
/**@type {SlashCommandScope}*/ scope;
|
||||||
|
|
||||||
|
/**@type {SlashCommandExecutor[]}*/ commandIndex;
|
||||||
|
|
||||||
|
get ahead() {
|
||||||
|
return this.text.slice(this.index + 1);
|
||||||
|
}
|
||||||
|
get behind() {
|
||||||
|
return this.text.slice(0, this.index);
|
||||||
|
}
|
||||||
|
get char() {
|
||||||
|
return this.text[this.index];
|
||||||
|
}
|
||||||
|
get endOfText() {
|
||||||
|
return this.index >= this.text.length || /^\s+$/.test(this.ahead);
|
||||||
|
}
|
||||||
|
|
||||||
|
addCommand(command, callback, aliases, helpString = '', interruptsGeneration = false, purgeFromMessage = true) {
|
||||||
|
if (['/', '#'].includes(command[0])) {
|
||||||
|
throw new Error(`Illegal Name. Slash commandn name cannot begin with "${command[0]}".`);
|
||||||
|
}
|
||||||
|
const fnObj = Object.assign(new SlashCommand(), { name:command, callback, helpString, interruptsGeneration, purgeFromMessage, aliases });
|
||||||
|
|
||||||
|
if ([command, ...aliases].some(x => Object.hasOwn(this.commands, x))) {
|
||||||
|
console.trace('WARN: Duplicate slash command registered!');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.commands[command] = fnObj;
|
||||||
|
|
||||||
|
if (Array.isArray(aliases)) {
|
||||||
|
aliases.forEach((alias) => {
|
||||||
|
this.commands[alias] = fnObj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let stringBuilder = `<span class="monospace">/${command}</span> ${helpString} `;
|
||||||
|
if (Array.isArray(aliases) && aliases.length) {
|
||||||
|
let aliasesString = `(alias: ${aliases.map(x => `<span class="monospace">/${x}</span>`).join(', ')})`;
|
||||||
|
stringBuilder += aliasesString;
|
||||||
|
}
|
||||||
|
this.helpStrings[command] = stringBuilder;
|
||||||
|
fnObj.helpStringFormatted = stringBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHelpString() {
|
||||||
|
const listItems = Object
|
||||||
|
.entries(this.helpStrings)
|
||||||
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
|
.map(x => x[1])
|
||||||
|
.map(x => `<li>${x}</li>`)
|
||||||
|
.join('\n');
|
||||||
|
return `<p>Slash commands:</p><ol>${listItems}</ol>
|
||||||
|
<small>Slash commands can be batched into a single input by adding a pipe character | at the end, and then writing a new slash command.</small>
|
||||||
|
<ul><li><small>Example:</small><code>/cut 1 | /sys Hello, | /continue</code></li>
|
||||||
|
<li>This will remove the first message in chat, send a system message that starts with 'Hello,', and then ask the AI to continue the message.</li></ul>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommandAt(text, index) {
|
||||||
|
try {
|
||||||
|
this.parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
index += 2;
|
||||||
|
return this.commandIndex.filter(it=>it.start <= index && (it.end >= index || it.end == null)).slice(-1)[0]
|
||||||
|
?? null
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
take(length = 1, keep = false) {
|
||||||
|
let content = '';
|
||||||
|
while (length-- > 0) {
|
||||||
|
content += this.char;
|
||||||
|
this.index++;
|
||||||
|
}
|
||||||
|
if (keep) this.keptText += content;
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
parse(text) {
|
||||||
|
this.text = `{:${text}:}`;
|
||||||
|
this.keptText = '';
|
||||||
|
this.index = 0;
|
||||||
|
this.scope = null;
|
||||||
|
this.commandIndex = [];
|
||||||
|
const closure = this.parseClosure();
|
||||||
|
console.log('[STS]', closure);
|
||||||
|
closure.keptText = this.keptText;
|
||||||
|
return closure;
|
||||||
|
}
|
||||||
|
|
||||||
|
testClosure() {
|
||||||
|
return this.ahead.length > 0 && this.char == '{' && this.ahead[0] == ':';
|
||||||
|
}
|
||||||
|
testClosureEnd() {
|
||||||
|
if (this.ahead.length < 1) throw new SlashCommandParserError(`Unclosed closure at position ${this.index - 2}`, this.text, this.index);
|
||||||
|
return this.char == ':' && this.ahead[0] == '}' && this.behind.slice(-1) != '\\';
|
||||||
|
}
|
||||||
|
parseClosure() {
|
||||||
|
this.take(2); // discard opening {:
|
||||||
|
let closure = new SlashCommandClosure(this.scope);
|
||||||
|
this.scope = closure.scope;
|
||||||
|
while (/\s/.test(this.char)) this.take(); // discard whitespace
|
||||||
|
while (!this.testClosureEnd()) {
|
||||||
|
if (this.testCommand()) {
|
||||||
|
closure.executorList.push(this.parseCommand());
|
||||||
|
} else {
|
||||||
|
while (!this.testCommandEnd()) this.take(1); // discard plain text and comments
|
||||||
|
}
|
||||||
|
while (/\s|\|/.test(this.char)) this.take(); // discard whitespace and pipe (command separator)
|
||||||
|
}
|
||||||
|
this.take(2); // discard closing :}
|
||||||
|
if (this.char == '(' && this.ahead[0] == ')') {
|
||||||
|
this.take(2); // discard ()
|
||||||
|
closure.executeNow = true;
|
||||||
|
}
|
||||||
|
while (/\s/.test(this.char)) this.take(); // discard trailing whitespace
|
||||||
|
this.scope = closure.scope.parent;
|
||||||
|
return closure;
|
||||||
|
}
|
||||||
|
|
||||||
|
testCommand() {
|
||||||
|
return this.char == '/' && this.behind.slice(-1) != '\\' && !['/', '#'].includes(this.ahead[0]);
|
||||||
|
}
|
||||||
|
testCommandEnd() {
|
||||||
|
return this.testClosureEnd() || this.endOfText || (this.char == '|' && this.behind.slice(-1) != '\\');
|
||||||
|
}
|
||||||
|
parseCommand() {
|
||||||
|
const start = this.index;
|
||||||
|
const cmd = new SlashCommandExecutor(start);
|
||||||
|
this.take(); // discard "/"
|
||||||
|
this.commandIndex.push(cmd);
|
||||||
|
while (!/\s/.test(this.char) && !this.testCommandEnd()) cmd.name += this.take(); // take chars until whitespace or end
|
||||||
|
while (/\s/.test(this.char)) this.take(); // discard whitespace
|
||||||
|
if (!this.commands[cmd.name]) throw new SlashCommandParserError(`Unknown command at position ${this.index - cmd.name.length - 2}: "/${cmd.name}"`, this.text, this.index - cmd.name.length);
|
||||||
|
cmd.command = this.commands[cmd.name];
|
||||||
|
while (this.testNamedArgument()) {
|
||||||
|
const arg = this.parseNamedArgument();
|
||||||
|
cmd.args[arg.key] = arg.value;
|
||||||
|
while (/\s/.test(this.char)) this.take(); // discard whitespace
|
||||||
|
}
|
||||||
|
while (/\s/.test(this.char)) this.take(); // discard whitespace
|
||||||
|
if (this.testUnnamedArgument()) {
|
||||||
|
cmd.value = this.parseUnnamedArgument();
|
||||||
|
}
|
||||||
|
if (this.testCommandEnd()) {
|
||||||
|
cmd.end = this.index;
|
||||||
|
if (!cmd.command.purgeFromMessage) this.keptText += this.text.slice(cmd.start, cmd.end);
|
||||||
|
return cmd;
|
||||||
|
} else {
|
||||||
|
console.warn(this.behind, this.char, this.ahead);
|
||||||
|
throw new SlashCommandParserError(`Unexpected end of command at position ${this.index - 2}: "/${cmd.command}"`, this.text, this.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testNamedArgument() {
|
||||||
|
return /^(\w+)=/.test(`${this.char}${this.ahead}`);
|
||||||
|
}
|
||||||
|
parseNamedArgument() {
|
||||||
|
let key = '';
|
||||||
|
while (/\w/.test(this.char)) key += this.take(); // take chars
|
||||||
|
this.take(); // discard "="
|
||||||
|
let value;
|
||||||
|
if (this.testClosure()) {
|
||||||
|
value = this.parseClosure();
|
||||||
|
} else if (this.testQuotedValue()) {
|
||||||
|
value = this.parseQuotedValue();
|
||||||
|
} else if (this.testListValue()) {
|
||||||
|
value = this.parseListValue();
|
||||||
|
} else if (this.testValue()) {
|
||||||
|
value = this.parseValue();
|
||||||
|
}
|
||||||
|
return { key, value };
|
||||||
|
}
|
||||||
|
|
||||||
|
testUnnamedArgument() {
|
||||||
|
return !this.testCommandEnd();
|
||||||
|
}
|
||||||
|
testUnnamedArgumentEnd() {
|
||||||
|
return this.testCommandEnd();
|
||||||
|
}
|
||||||
|
parseUnnamedArgument() {
|
||||||
|
/**@type {SlashCommandClosure|String}*/
|
||||||
|
let value = '';
|
||||||
|
let isList = false;
|
||||||
|
let listValues = [];
|
||||||
|
while (!this.testUnnamedArgumentEnd()) {
|
||||||
|
if (this.testClosure()) {
|
||||||
|
isList = true;
|
||||||
|
if (value.length > 0) {
|
||||||
|
listValues.push(value.trim());
|
||||||
|
value = '';
|
||||||
|
}
|
||||||
|
listValues.push(this.parseClosure());
|
||||||
|
} else {
|
||||||
|
value += this.take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isList && value.trim().length > 0) {
|
||||||
|
listValues.push(value.trim());
|
||||||
|
}
|
||||||
|
if (isList) {
|
||||||
|
if (listValues.length == 1) return listValues[0];
|
||||||
|
return listValues;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
testQuotedValue() {
|
||||||
|
return this.char == '"' && this.behind.slice(-1) != '\\';
|
||||||
|
}
|
||||||
|
testQuotedValueEnd() {
|
||||||
|
if (this.endOfText) throw new SlashCommandParserError(`Unexpected end of quoted value at position ${this.index}`, this.text, this.index);
|
||||||
|
return this.char == '"' && this.behind.slice(-1) != '\\';
|
||||||
|
}
|
||||||
|
parseQuotedValue() {
|
||||||
|
this.take(); // discard opening quote
|
||||||
|
let value = '';
|
||||||
|
while (!this.testQuotedValueEnd()) value += this.take(); // take all chars until closing quote
|
||||||
|
this.take(); // discard closing quote
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
testListValue() {
|
||||||
|
return this.char == '[' && this.behind.slice(-1) != '\\';
|
||||||
|
}
|
||||||
|
testListValueEnd() {
|
||||||
|
if (this.endOfText) throw new SlashCommandParserError(`Unexpected end of list value at position ${this.index}`, this.text, this.index);
|
||||||
|
return this.char == ']' && this.behind.slice(-1) != '\\';
|
||||||
|
}
|
||||||
|
parseListValue() {
|
||||||
|
let value = '';
|
||||||
|
while (!this.testListValueEnd()) value += this.take(); // take all chars until closing bracket
|
||||||
|
value += this.take(); // take closing bracket
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
testValue() {
|
||||||
|
return !/\s/.test(this.char);
|
||||||
|
}
|
||||||
|
testValueEnd() {
|
||||||
|
if (/\s/.test(this.char)) return true;
|
||||||
|
return this.testCommandEnd();
|
||||||
|
}
|
||||||
|
parseValue() {
|
||||||
|
let value = '';
|
||||||
|
while (!this.testValueEnd()) value += this.take(); // take all chars until value end
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
47
public/scripts/slash-commands/SlashCommandParserError.js
Normal file
47
public/scripts/slash-commands/SlashCommandParserError.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
export class SlashCommandParserError extends Error {
|
||||||
|
/**@type {String}*/ text;
|
||||||
|
/**@type {Number}*/ index;
|
||||||
|
|
||||||
|
get line() {
|
||||||
|
return this.text.slice(0, this.index).replace(/[^\n]/g, '').length;
|
||||||
|
}
|
||||||
|
get column() {
|
||||||
|
return this.text.slice(0, this.index).split('\n').pop().length;
|
||||||
|
}
|
||||||
|
get hint() {
|
||||||
|
let lineOffset = this.line.toString().length;
|
||||||
|
let lineStart = this.index;
|
||||||
|
let start = this.index;
|
||||||
|
let end = this.index;
|
||||||
|
let offset = 0;
|
||||||
|
let lineCount = 0;
|
||||||
|
while (offset < 10000 && lineCount < 3 && start >= 0) {
|
||||||
|
if (this.text[start] == '\n') lineCount++;
|
||||||
|
if (lineCount == 0) lineStart--;
|
||||||
|
offset++;
|
||||||
|
start--;
|
||||||
|
}
|
||||||
|
if (this.text[start + 1] == '\n') start++;
|
||||||
|
offset = 0;
|
||||||
|
while (offset < 10000 && this.text[end] != '\n') {
|
||||||
|
offset++;
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
let hint = [];
|
||||||
|
let lines = this.text.slice(start + 1, end - 1).split('\n');
|
||||||
|
let lineNum = this.line - lines.length + 1;
|
||||||
|
for (const line of lines) {
|
||||||
|
const num = `${' '.repeat(lineOffset - lineNum.toString().length)}${lineNum}`;
|
||||||
|
lineNum++;
|
||||||
|
hint.push(`${num}: ${line}`);
|
||||||
|
}
|
||||||
|
hint.push(`${' '.repeat(this.index - lineStart + lineOffset + 1)}^^^^^`);
|
||||||
|
return hint.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(message, text, index) {
|
||||||
|
super(message);
|
||||||
|
this.text = text.slice(2, -2);
|
||||||
|
this.index = index - 2;
|
||||||
|
}
|
||||||
|
}
|
102
public/scripts/slash-commands/SlashCommandScope.js
Normal file
102
public/scripts/slash-commands/SlashCommandScope.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
export class SlashCommandScope {
|
||||||
|
// @ts-ignore
|
||||||
|
/**@type {Map<String, Object>}*/ variables = {};
|
||||||
|
// @ts-ignore
|
||||||
|
/**@type {Map<String, Object>}*/ macros = {};
|
||||||
|
/**@type {SlashCommandScope}*/ parent;
|
||||||
|
/**@type {String}*/ #pipe;
|
||||||
|
get pipe() {
|
||||||
|
return this.#pipe ?? this.parent?.pipe;
|
||||||
|
}
|
||||||
|
set pipe(value) {
|
||||||
|
this.#pipe = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
constructor(parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCopy() {
|
||||||
|
const scope = new SlashCommandScope(this.parent);
|
||||||
|
scope.variables = Object.assign({}, this.variables);
|
||||||
|
scope.macros = this.macros;
|
||||||
|
scope.#pipe = this.#pipe;
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setMacro(key, value) {
|
||||||
|
this.macros[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
existsVariableInScope(key) {
|
||||||
|
return Object.keys(this.variables).includes(key);
|
||||||
|
}
|
||||||
|
existsVariable(key) {
|
||||||
|
return Object.keys(this.variables).includes(key) || this.parent?.existsVariable(key);
|
||||||
|
}
|
||||||
|
letVariable(key, value = undefined) {
|
||||||
|
if (this.existsVariableInScope(key)) throw new SlashCommandScopeVariableExistsError(`Variable named "${key}" already exists.`);
|
||||||
|
this.variables[key] = value;
|
||||||
|
}
|
||||||
|
setVariable(key, value, index = null) {
|
||||||
|
if (this.existsVariableInScope(key)) {
|
||||||
|
if (index !== null && index !== undefined) {
|
||||||
|
let v = this.variables[key];
|
||||||
|
try {
|
||||||
|
v = JSON.parse(v);
|
||||||
|
const numIndex = Number(index);
|
||||||
|
if (Number.isNaN(numIndex)) {
|
||||||
|
v[index] = value;
|
||||||
|
} else {
|
||||||
|
v[numIndex] = value;
|
||||||
|
}
|
||||||
|
v = JSON.stringify(v);
|
||||||
|
} catch {
|
||||||
|
v[index] = value;
|
||||||
|
}
|
||||||
|
this.variables[key] = v;
|
||||||
|
} else {
|
||||||
|
this.variables[key] = value;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (this.parent) {
|
||||||
|
return this.parent.setVariable(key, value, index);
|
||||||
|
}
|
||||||
|
throw new SlashCommandScopeVariableNotFoundError(`No such variable: "${key}"`);
|
||||||
|
}
|
||||||
|
getVariable(key, index = null) {
|
||||||
|
if (this.existsVariableInScope(key)) {
|
||||||
|
if (index !== null && index !== undefined) {
|
||||||
|
let v = this.variables[key];
|
||||||
|
try { v = JSON.parse(v); } catch { /* empty */ }
|
||||||
|
const numIndex = Number(index);
|
||||||
|
if (Number.isNaN(numIndex)) {
|
||||||
|
v = v[index];
|
||||||
|
} else {
|
||||||
|
v = v[numIndex];
|
||||||
|
}
|
||||||
|
if (typeof v == 'object') return JSON.stringify(v);
|
||||||
|
return v;
|
||||||
|
} else {
|
||||||
|
const value = this.variables[key];
|
||||||
|
return (value === '' || isNaN(Number(value))) ? (value || '') : Number(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.parent) {
|
||||||
|
return this.parent.getVariable(key, index);
|
||||||
|
}
|
||||||
|
throw new SlashCommandScopeVariableNotFoundError(`No such variable: "${key}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export class SlashCommandScopeVariableExistsError extends Error {}
|
||||||
|
|
||||||
|
|
||||||
|
export class SlashCommandScopeVariableNotFoundError extends Error {}
|
@ -1,6 +1,8 @@
|
|||||||
import { chat_metadata, getCurrentChatId, saveSettingsDebounced, sendSystemMessage, system_message_types } from '../script.js';
|
import { chat_metadata, getCurrentChatId, saveSettingsDebounced, sendSystemMessage, system_message_types } from '../script.js';
|
||||||
import { extension_settings, saveMetadataDebounced } from './extensions.js';
|
import { extension_settings, saveMetadataDebounced } from './extensions.js';
|
||||||
import { executeSlashCommands, registerSlashCommand } from './slash-commands.js';
|
import { executeSlashCommands, registerSlashCommand } from './slash-commands.js';
|
||||||
|
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
|
||||||
|
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
|
||||||
import { isFalseBoolean } from './utils.js';
|
import { isFalseBoolean } from './utils.js';
|
||||||
|
|
||||||
const MAX_LOOPS = 100;
|
const MAX_LOOPS = 100;
|
||||||
@ -189,9 +191,14 @@ function decrementGlobalVariable(name) {
|
|||||||
/**
|
/**
|
||||||
* Resolves a variable name to its value or returns the string as is if the variable does not exist.
|
* Resolves a variable name to its value or returns the string as is if the variable does not exist.
|
||||||
* @param {string} name Variable name
|
* @param {string} name Variable name
|
||||||
|
* @param {SlashCommandScope} scope Scope
|
||||||
* @returns {string} Variable value or the string literal
|
* @returns {string} Variable value or the string literal
|
||||||
*/
|
*/
|
||||||
export function resolveVariable(name) {
|
export function resolveVariable(name, scope = null) {
|
||||||
|
if (scope?.existsVariable(name)) {
|
||||||
|
return scope.getVariable(name);
|
||||||
|
}
|
||||||
|
|
||||||
if (existsLocalVariable(name)) {
|
if (existsLocalVariable(name)) {
|
||||||
return getLocalVariable(name);
|
return getLocalVariable(name);
|
||||||
}
|
}
|
||||||
@ -312,7 +319,8 @@ async function whileCallback(args, command) {
|
|||||||
const result = evalBoolean(rule, a, b);
|
const result = evalBoolean(rule, a, b);
|
||||||
|
|
||||||
if (result && command) {
|
if (result && command) {
|
||||||
await executeSubCommands(command);
|
if (command instanceof SlashCommandClosure) await command.execute();
|
||||||
|
else await executeSubCommands(command, args._scope);
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -322,13 +330,24 @@ async function whileCallback(args, command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function timesCallback(args, value) {
|
async function timesCallback(args, value) {
|
||||||
const [repeats, ...commandParts] = value.split(' ');
|
let repeats;
|
||||||
const command = commandParts.join(' ');
|
let command;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
[repeats, command] = value;
|
||||||
|
} else {
|
||||||
|
[repeats, ...command] = value.split(' ');
|
||||||
|
command = command.join(' ');
|
||||||
|
}
|
||||||
const isGuardOff = isFalseBoolean(args.guard);
|
const isGuardOff = isFalseBoolean(args.guard);
|
||||||
const iterations = Math.min(Number(repeats), isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS);
|
const iterations = Math.min(Number(repeats), isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS);
|
||||||
|
|
||||||
for (let i = 0; i < iterations; i++) {
|
for (let i = 0; i < iterations; i++) {
|
||||||
await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i));
|
if (command instanceof SlashCommandClosure) {
|
||||||
|
command.scope.setMacro('timesIndex', i);
|
||||||
|
await command.execute();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i), args._scope);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
@ -339,9 +358,11 @@ async function ifCallback(args, command) {
|
|||||||
const result = evalBoolean(rule, a, b);
|
const result = evalBoolean(rule, a, b);
|
||||||
|
|
||||||
if (result && command) {
|
if (result && command) {
|
||||||
return await executeSubCommands(command);
|
if (command instanceof SlashCommandClosure) return await command.execute();
|
||||||
} else if (!result && args.else && typeof args.else === 'string' && args.else !== '') {
|
return await executeSubCommands(command, args._scope);
|
||||||
return await executeSubCommands(args.else);
|
} else if (!result && args.else && ((typeof args.else === 'string' && args.else !== '') || args.else instanceof SlashCommandClosure)) {
|
||||||
|
if (args.else instanceof SlashCommandClosure) return await args.else.execute(args._scope);
|
||||||
|
return await executeSubCommands(args.else, args._scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
@ -386,6 +407,11 @@ function parseBooleanOperands(args) {
|
|||||||
return operandNumber;
|
return operandNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args._scope.existsVariable(operand)) {
|
||||||
|
const operandVariable = args._scope.getVariable(operand);
|
||||||
|
return operandVariable ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
if (existsLocalVariable(operand)) {
|
if (existsLocalVariable(operand)) {
|
||||||
const operandLocalVariable = getLocalVariable(operand);
|
const operandLocalVariable = getLocalVariable(operand);
|
||||||
return operandLocalVariable ?? '';
|
return operandLocalVariable ?? '';
|
||||||
@ -483,7 +509,7 @@ function evalBoolean(rule, a, b) {
|
|||||||
* @param {string} command Command to execute. May contain escaped macro and batch separators.
|
* @param {string} command Command to execute. May contain escaped macro and batch separators.
|
||||||
* @returns {Promise<string>} Pipe result
|
* @returns {Promise<string>} Pipe result
|
||||||
*/
|
*/
|
||||||
async function executeSubCommands(command) {
|
async function executeSubCommands(command, scope = null) {
|
||||||
if (command.startsWith('"')) {
|
if (command.startsWith('"')) {
|
||||||
command = command.slice(1);
|
command = command.slice(1);
|
||||||
}
|
}
|
||||||
@ -493,7 +519,7 @@ async function executeSubCommands(command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unescape = false;
|
const unescape = false;
|
||||||
const result = await executeSlashCommands(command, unescape);
|
const result = await executeSlashCommands(command, unescape, true, scope);
|
||||||
|
|
||||||
if (!result || typeof result !== 'object') {
|
if (!result || typeof result !== 'object') {
|
||||||
return '';
|
return '';
|
||||||
@ -537,9 +563,10 @@ function deleteGlobalVariable(name) {
|
|||||||
/**
|
/**
|
||||||
* Parses a series of numeric values from a string.
|
* Parses a series of numeric values from a string.
|
||||||
* @param {string} value A space-separated list of numeric values or variable names
|
* @param {string} value A space-separated list of numeric values or variable names
|
||||||
|
* @param {SlashCommandScope} scope Scope
|
||||||
* @returns {number[]} An array of numeric values
|
* @returns {number[]} An array of numeric values
|
||||||
*/
|
*/
|
||||||
function parseNumericSeries(value) {
|
function parseNumericSeries(value, scope = null) {
|
||||||
if (typeof value === 'number') {
|
if (typeof value === 'number') {
|
||||||
return [value];
|
return [value];
|
||||||
}
|
}
|
||||||
@ -548,18 +575,18 @@ function parseNumericSeries(value) {
|
|||||||
.split(' ')
|
.split(' ')
|
||||||
.map(i => i.trim())
|
.map(i => i.trim())
|
||||||
.filter(i => i !== '')
|
.filter(i => i !== '')
|
||||||
.map(i => isNaN(Number(i)) ? Number(resolveVariable(i)) : Number(i))
|
.map(i => isNaN(Number(i)) ? Number(resolveVariable(i, scope)) : Number(i))
|
||||||
.filter(i => !isNaN(i));
|
.filter(i => !isNaN(i));
|
||||||
|
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
function performOperation(value, operation, singleOperand = false) {
|
function performOperation(value, operation, singleOperand = false, scope = null) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const array = parseNumericSeries(value);
|
const array = parseNumericSeries(value, scope);
|
||||||
|
|
||||||
if (array.length === 0) {
|
if (array.length === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
@ -574,72 +601,72 @@ function performOperation(value, operation, singleOperand = false) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addValuesCallback(value) {
|
function addValuesCallback(args, value) {
|
||||||
return performOperation(value, (array) => array.reduce((a, b) => a + b, 0));
|
return performOperation(value, (array) => array.reduce((a, b) => a + b, 0), false, args._scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mulValuesCallback(value) {
|
function mulValuesCallback(args, value) {
|
||||||
return performOperation(value, (array) => array.reduce((a, b) => a * b, 1));
|
return performOperation(value, (array) => array.reduce((a, b) => a * b, 1), false, args._scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function minValuesCallback(value) {
|
function minValuesCallback(args, value) {
|
||||||
return performOperation(value, (array) => Math.min(...array));
|
return performOperation(value, (array) => Math.min(...array), false, args._scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function maxValuesCallback(value) {
|
function maxValuesCallback(args, value) {
|
||||||
return performOperation(value, (array) => Math.max(...array));
|
return performOperation(value, (array) => Math.max(...array), false, args._scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function subValuesCallback(value) {
|
function subValuesCallback(args, value) {
|
||||||
return performOperation(value, (array) => array[0] - array[1]);
|
return performOperation(value, (array) => array[0] - array[1], false, args._scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function divValuesCallback(value) {
|
function divValuesCallback(args, value) {
|
||||||
return performOperation(value, (array) => {
|
return performOperation(value, (array) => {
|
||||||
if (array[1] === 0) {
|
if (array[1] === 0) {
|
||||||
console.warn('Division by zero.');
|
console.warn('Division by zero.');
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return array[0] / array[1];
|
return array[0] / array[1];
|
||||||
});
|
}, false, args._scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function modValuesCallback(value) {
|
function modValuesCallback(args, value) {
|
||||||
return performOperation(value, (array) => {
|
return performOperation(value, (array) => {
|
||||||
if (array[1] === 0) {
|
if (array[1] === 0) {
|
||||||
console.warn('Division by zero.');
|
console.warn('Division by zero.');
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return array[0] % array[1];
|
return array[0] % array[1];
|
||||||
});
|
}, false, args._scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function powValuesCallback(value) {
|
function powValuesCallback(args, value) {
|
||||||
return performOperation(value, (array) => Math.pow(array[0], array[1]));
|
return performOperation(value, (array) => Math.pow(array[0], array[1]), false, args._scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sinValuesCallback(value) {
|
function sinValuesCallback(args, value) {
|
||||||
return performOperation(value, Math.sin, true);
|
return performOperation(value, Math.sin, true, args._scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cosValuesCallback(value) {
|
function cosValuesCallback(args, value) {
|
||||||
return performOperation(value, Math.cos, true);
|
return performOperation(value, Math.cos, true, args._scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function logValuesCallback(value) {
|
function logValuesCallback(args, value) {
|
||||||
return performOperation(value, Math.log, true);
|
return performOperation(value, Math.log, true, args._scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function roundValuesCallback(value) {
|
function roundValuesCallback(args, value) {
|
||||||
return performOperation(value, Math.round, true);
|
return performOperation(value, Math.round, true, args._scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function absValuesCallback(value) {
|
function absValuesCallback(args, value) {
|
||||||
return performOperation(value, Math.abs, true);
|
return performOperation(value, Math.abs, true, args._scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sqrtValuesCallback(value) {
|
function sqrtValuesCallback(args, value) {
|
||||||
return performOperation(value, Math.sqrt, true);
|
return performOperation(value, Math.sqrt, true, args._scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function lenValuesCallback(value) {
|
function lenValuesCallback(value) {
|
||||||
@ -696,20 +723,38 @@ export function registerVariableCommands() {
|
|||||||
registerSlashCommand('times', (args, value) => timesCallback(args, value), [], '<span class="monospace">(repeats) "(command)"</span> – execute any valid slash command enclosed in quotes <tt>repeats</tt> number of times, e.g. <tt>/setvar key=i 1 | /times 5 "/addvar key=i 1"</tt> adds 1 to the value of "i" 5 times. <tt>{{timesIndex}}</tt> is replaced with the iteration number (zero-based), e.g. <tt>/times 4 "/echo {{timesIndex}}"</tt> echos the numbers 0 through 4. Loops are limited to 100 iterations by default, pass guard=off to disable.', true, true);
|
registerSlashCommand('times', (args, value) => timesCallback(args, value), [], '<span class="monospace">(repeats) "(command)"</span> – execute any valid slash command enclosed in quotes <tt>repeats</tt> number of times, e.g. <tt>/setvar key=i 1 | /times 5 "/addvar key=i 1"</tt> adds 1 to the value of "i" 5 times. <tt>{{timesIndex}}</tt> is replaced with the iteration number (zero-based), e.g. <tt>/times 4 "/echo {{timesIndex}}"</tt> echos the numbers 0 through 4. Loops are limited to 100 iterations by default, pass guard=off to disable.', true, true);
|
||||||
registerSlashCommand('flushvar', (_, value) => deleteLocalVariable(value), [], '<span class="monospace">(key)</span> – delete a local variable, e.g. <tt>/flushvar score</tt>', true, true);
|
registerSlashCommand('flushvar', (_, value) => deleteLocalVariable(value), [], '<span class="monospace">(key)</span> – delete a local variable, e.g. <tt>/flushvar score</tt>', true, true);
|
||||||
registerSlashCommand('flushglobalvar', (_, value) => deleteGlobalVariable(value), [], '<span class="monospace">(key)</span> – delete a global variable, e.g. <tt>/flushglobalvar score</tt>', true, true);
|
registerSlashCommand('flushglobalvar', (_, value) => deleteGlobalVariable(value), [], '<span class="monospace">(key)</span> – delete a global variable, e.g. <tt>/flushglobalvar score</tt>', true, true);
|
||||||
registerSlashCommand('add', (_, value) => addValuesCallback(value), [], '<span class="monospace">(a b c d)</span> – performs an addition of the set of values and passes the result down the pipe, can use variable names, e.g. <tt>/add 10 i 30 j</tt>', true, true);
|
registerSlashCommand('add', (args, value) => addValuesCallback(args, value), [], '<span class="monospace">(a b c d)</span> – performs an addition of the set of values and passes the result down the pipe, can use variable names, e.g. <tt>/add 10 i 30 j</tt>', true, true);
|
||||||
registerSlashCommand('mul', (_, value) => mulValuesCallback(value), [], '<span class="monospace">(a b c d)</span> – performs a multiplication of the set of values and passes the result down the pipe, can use variable names, e.g. <tt>/mul 10 i 30 j</tt>', true, true);
|
registerSlashCommand('mul', (args, value) => mulValuesCallback(args, value), [], '<span class="monospace">(a b c d)</span> – performs a multiplication of the set of values and passes the result down the pipe, can use variable names, e.g. <tt>/mul 10 i 30 j</tt>', true, true);
|
||||||
registerSlashCommand('max', (_, value) => maxValuesCallback(value), [], '<span class="monospace">(a b c d)</span> – returns the maximum value of the set of values and passes the result down the pipe, can use variable names, e.g. <tt>/max 10 i 30 j</tt>', true, true);
|
registerSlashCommand('max', (args, value) => maxValuesCallback(args, value), [], '<span class="monospace">(a b c d)</span> – returns the maximum value of the set of values and passes the result down the pipe, can use variable names, e.g. <tt>/max 10 i 30 j</tt>', true, true);
|
||||||
registerSlashCommand('min', (_, value) => minValuesCallback(value), [], '<span class="monospace">(a b c d)</span> – returns the minimum value of the set of values and passes the result down the pipe, can use variable names, e.g. <tt>/min 10 i 30 j</tt>', true, true);
|
registerSlashCommand('min', (args, value) => minValuesCallback(args, value), [], '<span class="monospace">(a b c d)</span> – returns the minimum value of the set of values and passes the result down the pipe, can use variable names, e.g. <tt>/min 10 i 30 j</tt>', true, true);
|
||||||
registerSlashCommand('sub', (_, value) => subValuesCallback(value), [], '<span class="monospace">(a b)</span> – performs a subtraction of two values and passes the result down the pipe, can use variable names, e.g. <tt>/sub i 5</tt>', true, true);
|
registerSlashCommand('sub', (args, value) => subValuesCallback(args, value), [], '<span class="monospace">(a b)</span> – performs a subtraction of two values and passes the result down the pipe, can use variable names, e.g. <tt>/sub i 5</tt>', true, true);
|
||||||
registerSlashCommand('div', (_, value) => divValuesCallback(value), [], '<span class="monospace">(a b)</span> – performs a division of two values and passes the result down the pipe, can use variable names, e.g. <tt>/div 10 i</tt>', true, true);
|
registerSlashCommand('div', (args, value) => divValuesCallback(args, value), [], '<span class="monospace">(a b)</span> – performs a division of two values and passes the result down the pipe, can use variable names, e.g. <tt>/div 10 i</tt>', true, true);
|
||||||
registerSlashCommand('mod', (_, value) => modValuesCallback(value), [], '<span class="monospace">(a b)</span> – performs a modulo operation of two values and passes the result down the pipe, can use variable names, e.g. <tt>/mod i 2</tt>', true, true);
|
registerSlashCommand('mod', (args, value) => modValuesCallback(args, value), [], '<span class="monospace">(a b)</span> – performs a modulo operation of two values and passes the result down the pipe, can use variable names, e.g. <tt>/mod i 2</tt>', true, true);
|
||||||
registerSlashCommand('pow', (_, value) => powValuesCallback(value), [], '<span class="monospace">(a b)</span> – performs a power operation of two values and passes the result down the pipe, can use variable names, e.g. <tt>/pow i 2</tt>', true, true);
|
registerSlashCommand('pow', (args, value) => powValuesCallback(args, value), [], '<span class="monospace">(a b)</span> – performs a power operation of two values and passes the result down the pipe, can use variable names, e.g. <tt>/pow i 2</tt>', true, true);
|
||||||
registerSlashCommand('sin', (_, value) => sinValuesCallback(value), [], '<span class="monospace">(a)</span> – performs a sine operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/sin i</tt>', true, true);
|
registerSlashCommand('sin', (args, value) => sinValuesCallback(args, value), [], '<span class="monospace">(a)</span> – performs a sine operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/sin i</tt>', true, true);
|
||||||
registerSlashCommand('cos', (_, value) => cosValuesCallback(value), [], '<span class="monospace">(a)</span> – performs a cosine operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/cos i</tt>', true, true);
|
registerSlashCommand('cos', (args, value) => cosValuesCallback(args, value), [], '<span class="monospace">(a)</span> – performs a cosine operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/cos i</tt>', true, true);
|
||||||
registerSlashCommand('log', (_, value) => logValuesCallback(value), [], '<span class="monospace">(a)</span> – performs a logarithm operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/log i</tt>', true, true);
|
registerSlashCommand('log', (args, value) => logValuesCallback(args, value), [], '<span class="monospace">(a)</span> – performs a logarithm operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/log i</tt>', true, true);
|
||||||
registerSlashCommand('abs', (_, value) => absValuesCallback(value), [], '<span class="monospace">(a)</span> – performs an absolute value operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/abs i</tt>', true, true);
|
registerSlashCommand('abs', (args, value) => absValuesCallback(args, value), [], '<span class="monospace">(a)</span> – performs an absolute value operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/abs i</tt>', true, true);
|
||||||
registerSlashCommand('sqrt', (_, value) => sqrtValuesCallback(value), [], '<span class="monospace">(a)</span> – performs a square root operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/sqrt i</tt>', true, true);
|
registerSlashCommand('sqrt', (args, value) => sqrtValuesCallback(args, value), [], '<span class="monospace">(a)</span> – performs a square root operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/sqrt i</tt>', true, true);
|
||||||
registerSlashCommand('round', (_, value) => roundValuesCallback(value), [], '<span class="monospace">(a)</span> – rounds a value and passes the result down the pipe, can use variable names, e.g. <tt>/round i</tt>', true, true);
|
registerSlashCommand('round', (args, value) => roundValuesCallback(args, value), [], '<span class="monospace">(a)</span> – rounds a value and passes the result down the pipe, can use variable names, e.g. <tt>/round i</tt>', true, true);
|
||||||
registerSlashCommand('len', (_, value) => lenValuesCallback(value), [], '<span class="monospace">(a)</span> – gets the length of a value and passes the result down the pipe, can use variable names, e.g. <tt>/len i</tt>', true, true);
|
registerSlashCommand('len', (_, value) => lenValuesCallback(value), [], '<span class="monospace">(a)</span> – gets the length of a value and passes the result down the pipe, can use variable names, e.g. <tt>/len i</tt>', true, true);
|
||||||
registerSlashCommand('rand', (args, value) => randValuesCallback(Number(args.from ?? 0), Number(args.to ?? (value.length ? value : 1)), args), [], '<span class="monospace">(from=number=0 to=number=1 round=round|ceil|floor)</span> – returns a random number between from and to, e.g. <tt>/rand</tt> or <tt>/rand 10</tt> or <tt>/rand from=5 to=10</tt>', true, true);
|
registerSlashCommand('rand', (args, value) => randValuesCallback(Number(args.from ?? 0), Number(args.to ?? (value.length ? value : 1)), args), [], '<span class="monospace">(from=number=0 to=number=1 round=round|ceil|floor)</span> – returns a random number between from and to, e.g. <tt>/rand</tt> or <tt>/rand 10</tt> or <tt>/rand from=5 to=10</tt>', true, true);
|
||||||
|
registerSlashCommand('var', (args, value) => {
|
||||||
|
if (value.includes(' ')) {
|
||||||
|
const key = value.split(' ')[0];
|
||||||
|
const val = value.split(' ').slice(1).join(' ');
|
||||||
|
args._scope.setVariable(key, val, args.index);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
return args._scope.getVariable(value, args.index);
|
||||||
|
}, [], '<span class="monospace">[optional index] (variableName) (optional variable value)</span> – Get or set a variable. Example: <code>/let x foo | /var x foo bar | /var x | /echo</code>', true, true);
|
||||||
|
registerSlashCommand('let', (args, value) => {
|
||||||
|
if (value.includes(' ')) {
|
||||||
|
const key = value.split(' ')[0];
|
||||||
|
const val = value.split(' ').slice(1).join(' ');
|
||||||
|
args._scope.letVariable(key, val);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
args._scope.letVariable(value);
|
||||||
|
}, [], '<span class="monospace">(variableName) (optional variable value)</span> – Declares a new variable in the current scope. Example: <code>/let x foo bar | /echo {{var::x}}</code> or <code>/let y</code>', true, true);
|
||||||
}
|
}
|
||||||
|
@ -1038,6 +1038,31 @@ select {
|
|||||||
order: 3;
|
order: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slashCommandAutoComplete {
|
||||||
|
--bottom: 50vh;
|
||||||
|
background: black;
|
||||||
|
border: 1px solid white;
|
||||||
|
color: rgb(204 204 204);
|
||||||
|
max-height: calc(95vh - var(--bottom));
|
||||||
|
list-style: none;
|
||||||
|
margin: 0px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0px;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10000;
|
||||||
|
|
||||||
|
> .item {
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgb(43 45 46);
|
||||||
|
}
|
||||||
|
&.selected {
|
||||||
|
background-color: #20395C;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#character_popup .editor_maximize {
|
#character_popup .editor_maximize {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
|
Reference in New Issue
Block a user