diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 2b48a4719..d7b6543a3 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1740,14 +1740,14 @@ function modelCallback(_, model) { * @param {SlashCommandScope} scope The scope to be used when executing the commands. * @returns {Promise} */ -async function executeSlashCommands(text, handleParserErrors = true, scope = null, handleExecutionErrors = false) { +async function executeSlashCommands(text, handleParserErrors = true, scope = null, handleExecutionErrors = false, parserFlags = null) { if (!text) { return null; } let closure; try { - closure = parser.parse(text); + closure = parser.parse(text, true, parserFlags); closure.scope.parent = scope; } catch (e) { if (handleParserErrors && e instanceof SlashCommandParserError) { diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index 1257e1b2e..1ddfcbd42 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -153,6 +153,7 @@ export class SlashCommandClosure { interrupt = executor.command.interruptsGeneration; let args = { _scope: this.scope, + _parserFlags: executor.parserFlags, }; let value; // substitute named arguments diff --git a/public/scripts/slash-commands/SlashCommandExecutor.js b/public/scripts/slash-commands/SlashCommandExecutor.js index aa78aebfb..ac635a87d 100644 --- a/public/scripts/slash-commands/SlashCommandExecutor.js +++ b/public/scripts/slash-commands/SlashCommandExecutor.js @@ -2,6 +2,7 @@ import { SlashCommand } from './SlashCommand.js'; // eslint-disable-next-line no-unused-vars import { SlashCommandClosure } from './SlashCommandClosure.js'; +import { PARSER_FLAG } from './SlashCommandParser.js'; export class SlashCommandExecutor { /**@type {Boolean}*/ injectPipe = true; @@ -12,6 +13,7 @@ export class SlashCommandExecutor { // @ts-ignore /**@type {Object.}*/ args = {}; /**@type {String|SlashCommandClosure|(String|SlashCommandClosure)[]}*/ value; + /**@type {Object} */ parserFlags; constructor(start) { this.start = start; diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 4c5b77680..25e2be783 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -1,3 +1,4 @@ +import { isTrueBoolean } from '../utils.js'; import { SlashCommand } from './SlashCommand.js'; import { OPTION_TYPE, SlashCommandAutoCompleteOption } from './SlashCommandAutoCompleteOption.js'; import { SlashCommandClosure } from './SlashCommandClosure.js'; @@ -7,6 +8,12 @@ import { NAME_RESULT_TYPE, SlashCommandParserNameResult } from './SlashCommandPa // eslint-disable-next-line no-unused-vars import { SlashCommandScope } from './SlashCommandScope.js'; +/**@readonly*/ +/**@enum {Number}*/ +export const PARSER_FLAG = { + 'STRICT_ESCAPING': 1, +}; + export class SlashCommandParser { // @ts-ignore /**@type {Object.}*/ commands = {}; @@ -18,6 +25,8 @@ export class SlashCommandParser { /**@type {number}*/ index; /**@type {SlashCommandScope}*/ scope; + /**@type {Object.}*/ flags = {}; + /**@type {boolean}*/ jumpedEscapeSequence = false; /**@type {{start:number, end:number}[]}*/ closureIndex; @@ -253,6 +262,7 @@ export class SlashCommandParser { * @returns Whether the next characters are the indicated symbol. */ testSymbol(sequence, offset = 0) { + if (!this.flags[PARSER_FLAG.STRICT_ESCAPING]) return this.testSymbolLooseyGoosey(sequence, offset); // /echo abc | /echo def // -> TOAST: abc // -> TOAST: def @@ -294,9 +304,35 @@ export class SlashCommandParser { } } + testSymbolLooseyGoosey(sequence, offset = 0) { + const escapeOffset = this.jumpedEscapeSequence ? -1 : 0; + const escapes = this.text[this.index + offset + escapeOffset] == '\\' ? 1 : 0; + const test = (sequence instanceof RegExp) ? + (text) => new RegExp(`^${sequence.source}`).test(text) : + (text) => text.startsWith(sequence) + ; + if (test(this.text.slice(this.index + offset + escapeOffset + escapes))) { + // no backslashes before sequence + // -> sequence found + if (escapes == 0) return true; + // otherwise + // -> sequence found + if (!this.jumpedEscapeSequence && offset == 0) { + this.index++; + this.jumpedEscapeSequence = true; + } + return false; + } + } - parse(text, verifyCommandNames = true) { + + parse(text, verifyCommandNames = true, flags = null) { this.verifyCommandNames = verifyCommandNames; + if (flags) { + for (const key of Object.keys(PARSER_FLAG)) { + this.flags[PARSER_FLAG[key]] = flags[PARSER_FLAG[key]] ?? false; + } + } this.text = `{:${text}:}`; this.keptText = ''; this.index = 0; @@ -331,7 +367,11 @@ export class SlashCommandParser { this.discardWhitespace(); } while (!this.testClosureEnd()) { - if (this.testRunShorthand()) { + if (this.testComment()) { + this.parseComment(); + } else if (this.testParserFlag()) { + this.parseParserFlag(); + } else if (this.testRunShorthand()) { const cmd = this.parseRunShorthand(); closure.executorList.push(cmd); injectPipe = true; @@ -366,6 +406,44 @@ export class SlashCommandParser { return closure; } + testComment() { + return this.testSymbol(/\/[/#]/); + } + testCommentEnd() { + return this.testCommandEnd(); + } + parseComment() { + const start = this.index + 2; + const cmd = new SlashCommandExecutor(start); + this.commandIndex.push(cmd); + this.scopeIndex.push(this.scope.getCopy()); + this.take(); // discard "/" + cmd.name = this.take(); // set second "/" or "#" as name + while (!this.testCommentEnd()) this.take(); + cmd.end = this.index; + } + + testParserFlag() { + return this.testSymbol('/parser-flag '); + } + testParserFlagEnd() { + return this.testCommandEnd(); + } + parseParserFlag() { + const start = this.index + 1; + const cmd = new SlashCommandExecutor(start); + cmd.name = 'parser-flag'; + cmd.value = ''; + this.commandIndex.push(cmd); + this.scopeIndex.push(this.scope.getCopy()); + this.take(13); // discard "/parser-flag " + const [flag, state] = this.parseUnnamedArgument()?.split(/\s+/) ?? [null, null]; + if (Object.keys(PARSER_FLAG).includes(flag)) { + this.flags[PARSER_FLAG[flag]] = isTrueBoolean(state); + } + cmd.end = this.index; + } + testRunShorthand() { return this.testSymbol('/:') && !this.testSymbol(':}', 1); } @@ -402,7 +480,7 @@ export class SlashCommandParser { } testCommand() { - return this.testSymbol('/') && !this.testSymbol('//') && !this.testSymbol('/#'); + return this.testSymbol('/'); } testCommandEnd() { return this.testClosureEnd() || this.testSymbol('|'); @@ -410,6 +488,7 @@ export class SlashCommandParser { parseCommand() { const start = this.index + 1; const cmd = new SlashCommandExecutor(start); + cmd.parserFlags = Object.assign({}, this.flags); this.commandIndex.push(cmd); this.scopeIndex.push(this.scope.getCopy()); this.take(); // discard "/" diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 3f154056c..e12d0ed54 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -320,7 +320,7 @@ async function whileCallback(args, command) { if (result && command) { if (command instanceof SlashCommandClosure) await command.execute(); - else await executeSubCommands(command, args._scope); + else await executeSubCommands(command, args._scope, args._parserFlags); } else { break; } @@ -346,7 +346,7 @@ async function timesCallback(args, value) { await command.execute(); } else { - await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i), args._scope); + await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i), args._scope, args._parserFlags); } } @@ -359,10 +359,10 @@ async function ifCallback(args, command) { if (result && command) { if (command instanceof SlashCommandClosure) return (await command.execute()).pipe; - return await executeSubCommands(command, args._scope); + return await executeSubCommands(command, args._scope, args._parserFlags); } 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)).pipe; - return await executeSubCommands(args.else, args._scope); + return await executeSubCommands(args.else, args._scope, args._parserFlags); } return ''; @@ -509,12 +509,12 @@ function evalBoolean(rule, a, b) { * @param {string} command Command to execute. May contain escaped macro and batch separators. * @returns {Promise} Pipe result */ -async function executeSubCommands(command, scope = null) { +async function executeSubCommands(command, scope = null, parserFlags = null) { if (command.startsWith('"') && command.endsWith('"')) { command = command.slice(1, -1); } - const result = await executeSlashCommands(command, true, scope); + const result = await executeSlashCommands(command, true, scope, true, parserFlags); if (!result || typeof result !== 'object') { return '';