mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
STscript Parser Rewrite (#1965)
* set isForced to true on input * make floating auto-complete follow horizontal scrolling * add callable closure vars * changes to /let and /var for callable closures * fix error message * fix scope for closure arguments * if should return the pipe result from closures * use /run to call closures and no arguments on immediate closures * throw exception from QRs window-function if no match * when to show autocomplete vs info only * autocomplete positioning * autocomplete styling * add theming to autocomplete (theme, dark, light) * improve autocomplete show/hide logic and editor selection * use blur tint color instead of chat tint color and use blur setting * cleanup and docs * use scope macros for QR args * add enter to select autocomplete * fix no executor found * cleanup and comment * fix alias list in help string * fallback to empty string piped value if null or undefined * fix typo * blur textarea on ctrl+enter execute (and refocus after) * stop executeSlashCommand if parser throws * move /let and /var callbacks into functions * switch textarea to monospace when value starts with slash * add double pipe a pipe breaker * fix /? slash * remove some logging * add "/:name" as shorthand for "/run name" after all * move shit around * fix error message * use testRunShorthandEnd * use parseQuotedValue and parseValue to determine name for "/:" QR labels and set names can include spaces * add some adjustments to make autocomplete work properly some hint in there about "/:" would still be nice * add autocomplete style selector * only strip quotes from subcommand if they are at both ends * fix JSDoc * escaping * allow open quotes on dry run * throwing shit at the wall for /: autocomplete * escapes only for symbols * clean up autocomplete * improve performance * fix scope macros * remove unescaping of pipes * fix macros in scope copy * fix "/? slash" * don't run parser for getNameAt if text has not changed * fix options filter * re-enable blur listener * restore selection on non-replace select * fix for escaping first character of value * add support for {{pipe}} and {{var::}} closures * add index support to var macro * add scoped var macro to macro help * more escape fixes * reduce autocomplete render debounce * cleanup * restore old escape handling and parser flag for strict escaping * fix "no match" autocomplete message * add dummy commands for comments and parser flag * fix type annotations * somewhat safer macro replacements * fix autocomplete select on blank / "no match" * fix cutting off handled part in substitution * add parser flag REPLACE_GETVAR Replaces all {{getvar::}} and {{getglobalvar::}} macros with {{var::}}. Inserts a series of command executors before the command with the macros that: - save {{pipe}} to a var - call /getvar or /getglobalvar to get the variable used in the macro - call /let to save the retrieved variable - return the saved {{pipe}} value This helps to avoid double-substitutions when the var values contain text that could be interpreted as macros. * remove old parser * fix send on enter when no match * deal with pipes in quoted values (loose escaping) * add default parser flags to user settings * allow quoted values in unnamed argument * set parser flag without explicit state to "on" * add click hint on parser error toast * dirty more detailed cmd defs * remove name from unnamed arg * move autocomplete into class and floating with details * replace jQuery's trigger('input') on #send_textarea with native events because jQuery does not dispatch the native event * fix ctrl+space * fix arrow navigation * add comments * fix pointer block * add static fromProps * fix up dummy commands * migrate all commands to addCommandObject * remove commented comment command * fix alias in details * add range as argument type * switch to addCommandObject * switch to addCommandObject * fix height * fix floating details position on left * re-enable blur event * use auto width for full details on floating autocomplete * auto-size floating full details * fix typo * re-enable blur listener * don't prevent enter when selected item is fully typed out * add autocomplete details tooltips * add language to slash command examples * move makeItem into option and command and fix click select * use autocomplete parts in /? slash * fix alias formatting * add language to slash command examples * fix details position on initial input history * small screen styles * replace registerSlashCommand with detailed declarations * put name on first line * add missing returns * fix missing comma * fix alias display in autocomplete list * remove args from help string * move parser settings to its own section * jsdoc * hljs stscript lang * add hljs to autocomplete help examples * add missing import * apply autocomplete colors to stscript codeblocks (hljs) * add fromProps * cache autocomplete elements * towards generic autocomplete * remove unused imports * fix blanks * add return types * re-enable blur * fix blank check * Caption messages by id * add aborting command execution * fix return type * fix chat input font reset * add slash command progress indicator * add missing return * mark registerSlashCommand deprecated * why?? * separate abort logic for commands * remove parsing of quoted values from unnamed arg * add adjustable autocomplete width * revert stop button pulse * add progress and pause/abort to QR editor * add resize event on autocomplete width change * add key= argument to all get vars * refactoring * introduce NamedArgumentAsignment * add TODOs * refactoring * record start and end of named arg assignment * refactoring * prevent duplicate calls to show * refactoring * remove macro ac * add secondary autocomplete and enum descriptions * add syntax highlighting to QR editor * add enum descriptions to /while * add /let key=... to scope variable names * add unnamed argument assignment class and unnamed argument splitting * fix QR editor style * remove dash before autocomplete help text * add autocomplete for unnamed enums * fix remaining dom after holding backslash * fix for unnamed enums * fix autocomplete for /parser-flag * add parser-flag enum help * fix type annotations * fix autocomplete result for /: * add colored autocomplete type icons * collapse second line autocomplete help if empty * mark optional named args in autocomplete * fix when what * remove duplicate debug buttons * dispatch input on autocomplete select * prevent grow from editor syntax layer * add auto-adjust qr editor caret color * remove text-shadow from autocomplete * join value strings in /let and /var * add /abort syntax highlight * fix attempting secondary result when there is none * rename settings headers and split autocomplete / stscript * add parser flag tooltips * add tooltips to chat width stops * fix typo * return clone of help item * fix enum string * don't make optional notice for autocomplete arguments smaller * avoid scrollbar in chat input * add rudimentary macro autocomplete * strip macro from helptext * finally remove closure delimiters around root * cleanup * fix index stuff for removed closure delimiters * fix type hint * add child commands to progress indicator * include sub-separator in macro autocomplete * remove all mentions of interruptsGeneration and purge * remove unused imports * fix syntax highlight with newline at end of input * cleanup select pointer events * coalesce onProgress call * add regex to STscript syntax highlighting * fix closure end * fix autocomplete type icon alignment * adjustments for small screens * fix removing wrong element * add missing "at=" arg to /sys, /comment, /sendas * add font scale setting for autocomplete * add target=_blank for parser flag links * fix for searching enums * remove REGEXP_MODE from hljs just causes trouble * fix autocomplete in closures * fix typo * fix type hint * Get rid of scroll bar on load * Add type hint for /send name argument. Fix 'at' types * Add 'negative' arg hint to /sd command * reenable blur event * Allow /summarize to process any text * Compact layout of script toggles * Expand CSS by default * fix double ranger indicator and adjust to narrow container * make custom css input fill available vertical space * reduce scroll lag * use default cursor on scrollbar * Clean-up module loading in index.html * fix tab indent with hljs --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
This commit is contained in:
949
public/scripts/slash-commands/SlashCommandParser.js
Normal file
949
public/scripts/slash-commands/SlashCommandParser.js
Normal file
@ -0,0 +1,949 @@
|
||||
import { power_user } from '../power-user.js';
|
||||
import { isTrueBoolean, uuidv4 } from '../utils.js';
|
||||
import { SlashCommand } from './SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument } from './SlashCommandArgument.js';
|
||||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
||||
import { SlashCommandParserError } from './SlashCommandParserError.js';
|
||||
import { AutoCompleteNameResult } from '../autocomplete/AutoCompleteNameResult.js';
|
||||
import { SlashCommandQuickReplyAutoCompleteOption } from './SlashCommandQuickReplyAutoCompleteOption.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { SlashCommandScope } from './SlashCommandScope.js';
|
||||
import { SlashCommandVariableAutoCompleteOption } from './SlashCommandVariableAutoCompleteOption.js';
|
||||
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
|
||||
import { SlashCommandAutoCompleteNameResult } from './SlashCommandAutoCompleteNameResult.js';
|
||||
import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js';
|
||||
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
|
||||
import { MacroAutoCompleteOption } from '../autocomplete/MacroAutoCompleteOption.js';
|
||||
|
||||
/**@readonly*/
|
||||
/**@enum {Number}*/
|
||||
export const PARSER_FLAG = {
|
||||
'STRICT_ESCAPING': 1,
|
||||
'REPLACE_GETVAR': 2,
|
||||
};
|
||||
|
||||
export class SlashCommandParser {
|
||||
// @ts-ignore
|
||||
/**@type {Object.<string, SlashCommand>}*/ static commands = {};
|
||||
|
||||
/**
|
||||
* @deprecated Use SlashCommandParser.addCommandObject() instead.
|
||||
* @param {string} command Command name
|
||||
* @param {(namedArguments:Object.<string,string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} callback The function to execute when the command is called
|
||||
* @param {string[]} aliases List of alternative command names
|
||||
* @param {string} helpString Help text shown in autocomplete and command browser
|
||||
*/
|
||||
static addCommand(command, callback, aliases, helpString = '') {
|
||||
this.addCommandObject(SlashCommand.fromProps({
|
||||
name: command,
|
||||
callback,
|
||||
aliases,
|
||||
helpString,
|
||||
}));
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {SlashCommand} command
|
||||
*/
|
||||
static addCommandObject(command) {
|
||||
const reserved = ['/', '#', ':', 'parser-flag'];
|
||||
for (const start of reserved) {
|
||||
if (command.name.toLowerCase().startsWith(start) || (command.aliases ?? []).find(a=>a.toLowerCase().startsWith(start))) {
|
||||
throw new Error(`Illegal Name. Slash command name cannot begin with "${start}".`);
|
||||
}
|
||||
}
|
||||
this.addCommandObjectUnsafe(command);
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {SlashCommand} command
|
||||
*/
|
||||
static addCommandObjectUnsafe(command) {
|
||||
if ([command.name, ...command.aliases].some(x => Object.hasOwn(this.commands, x))) {
|
||||
console.trace('WARN: Duplicate slash command registered!', [command.name, ...command.aliases]);
|
||||
}
|
||||
|
||||
this.commands[command.name] = command;
|
||||
|
||||
if (Array.isArray(command.aliases)) {
|
||||
command.aliases.forEach((alias) => {
|
||||
this.commands[alias] = command;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get commands() {
|
||||
return SlashCommandParser.commands;
|
||||
}
|
||||
// @ts-ignore
|
||||
/**@type {Object.<string, string>}*/ helpStrings = {};
|
||||
/**@type {boolean}*/ verifyCommandNames = true;
|
||||
/**@type {string}*/ text;
|
||||
/**@type {number}*/ index;
|
||||
/**@type {SlashCommandAbortController}*/ abortController;
|
||||
/**@type {SlashCommandScope}*/ scope;
|
||||
/**@type {SlashCommandClosure}*/ closure;
|
||||
|
||||
/**@type {Object.<PARSER_FLAG,boolean>}*/ flags = {};
|
||||
|
||||
/**@type {boolean}*/ jumpedEscapeSequence = false;
|
||||
|
||||
/**@type {{start:number, end:number}[]}*/ closureIndex;
|
||||
/**@type {{start:number, end:number, name:string}[]}*/ macroIndex;
|
||||
/**@type {SlashCommandExecutor[]}*/ commandIndex;
|
||||
/**@type {SlashCommandScope[]}*/ scopeIndex;
|
||||
|
||||
get userIndex() { return this.index; }
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
constructor() {
|
||||
//TODO should not be re-registered from every instance
|
||||
// add dummy commands for help strings / autocomplete
|
||||
if (!Object.keys(this.commands).includes('parser-flag')) {
|
||||
const help = {};
|
||||
help[PARSER_FLAG.REPLACE_GETVAR] = 'Replace all {{getvar::}} and {{getglobalvar::}} macros with scoped variables to avoid double macro substitution.';
|
||||
help[PARSER_FLAG.STRICT_ESCAPING] = 'Allows to escape all delimiters with backslash, and allows escaping of backslashes.';
|
||||
SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: 'parser-flag',
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'The parser flag to modify.',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
isRequired: true,
|
||||
enumList: Object.keys(PARSER_FLAG).map(flag=>new SlashCommandEnumValue(flag, help[PARSER_FLAG[flag]])),
|
||||
}),
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'The state of the parser flag to set.',
|
||||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||
defaultValue: 'on',
|
||||
enumList: ['on', 'off'],
|
||||
}),
|
||||
],
|
||||
splitUnnamedArgument: true,
|
||||
helpString: 'Set a parser flag.',
|
||||
}));
|
||||
}
|
||||
if (!Object.keys(this.commands).includes('/')) {
|
||||
SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: '/',
|
||||
aliases: ['#'],
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'commentary',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
}),
|
||||
],
|
||||
helpString: 'Write a comment.',
|
||||
}));
|
||||
}
|
||||
|
||||
//TODO should not be re-registered from every instance
|
||||
this.registerLanguage();
|
||||
}
|
||||
registerLanguage() {
|
||||
// NUMBER mode is copied from highlightjs's own implementation for JavaScript
|
||||
// https://tc39.es/ecma262/#sec-literals-numeric-literals
|
||||
const decimalDigits = '[0-9](_?[0-9])*';
|
||||
const frac = `\\.(${decimalDigits})`;
|
||||
// DecimalIntegerLiteral, including Annex B NonOctalDecimalIntegerLiteral
|
||||
// https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
|
||||
const decimalInteger = '0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*';
|
||||
const NUMBER = {
|
||||
className: 'number',
|
||||
variants: [
|
||||
// DecimalLiteral
|
||||
{ begin: `(\\b(${decimalInteger})((${frac})|\\.)?|(${frac}))` +
|
||||
`[eE][+-]?(${decimalDigits})\\b` },
|
||||
{ begin: `\\b(${decimalInteger})\\b((${frac})\\b|\\.)?|(${frac})\\b` },
|
||||
|
||||
// DecimalBigIntegerLiteral
|
||||
{ begin: '\\b(0|[1-9](_?[0-9])*)n\\b' },
|
||||
|
||||
// NonDecimalIntegerLiteral
|
||||
{ begin: '\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b' },
|
||||
{ begin: '\\b0[bB][0-1](_?[0-1])*n?\\b' },
|
||||
{ begin: '\\b0[oO][0-7](_?[0-7])*n?\\b' },
|
||||
|
||||
// LegacyOctalIntegerLiteral (does not include underscore separators)
|
||||
// https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
|
||||
{ begin: '\\b0[0-7]+n?\\b' },
|
||||
],
|
||||
relevance: 0,
|
||||
};
|
||||
|
||||
const COMMENT = {
|
||||
scope: 'comment',
|
||||
begin: /\/[/#]/,
|
||||
end: /\||$|:}/,
|
||||
contains: [],
|
||||
};
|
||||
const ABORT = {
|
||||
scope: 'abort',
|
||||
begin: /\/abort/,
|
||||
end: /\||$|:}/,
|
||||
contains: [],
|
||||
};
|
||||
const LET = {
|
||||
begin: [
|
||||
/\/(let|var)\s+/,
|
||||
],
|
||||
beginScope: {
|
||||
1: 'variable',
|
||||
},
|
||||
end: /\||$|:}/,
|
||||
contains: [],
|
||||
};
|
||||
const SETVAR = {
|
||||
begin: /\/(setvar|setglobalvar)\s+/,
|
||||
beginScope: 'variable',
|
||||
end: /\||$|:}/,
|
||||
excludeEnd: true,
|
||||
contains: [],
|
||||
};
|
||||
const GETVAR = {
|
||||
begin: /\/(getvar|getglobalvar)\s+/,
|
||||
beginScope: 'variable',
|
||||
end: /\||$|:}/,
|
||||
excludeEnd: true,
|
||||
contains: [],
|
||||
};
|
||||
const RUN = {
|
||||
match: [
|
||||
/\/:/,
|
||||
/(".+?(?<!\\)") |(\S+?) /,
|
||||
],
|
||||
className: {
|
||||
1: 'variable.language',
|
||||
2: 'title.function.invoke',
|
||||
},
|
||||
contains: [], // defined later
|
||||
};
|
||||
const COMMAND = {
|
||||
scope: 'command',
|
||||
begin: /\/\S+/,
|
||||
beginScope: 'title.function',
|
||||
end: /\||$|(?=:})/,
|
||||
excludeEnd: true,
|
||||
contains: [], // defined later
|
||||
};
|
||||
const CLOSURE = {
|
||||
scope: 'closure',
|
||||
begin: /{:/,
|
||||
end: /:}(\(\))?/,
|
||||
beginScope: 'punctuation',
|
||||
endScope: 'punctuation',
|
||||
contains: [], // defined later
|
||||
};
|
||||
const NAMED_ARG = {
|
||||
scope: 'property',
|
||||
begin: /\w+=/,
|
||||
end: '',
|
||||
};
|
||||
const MACRO = {
|
||||
scope: 'variable',
|
||||
begin: /{{/,
|
||||
end: /}}/,
|
||||
};
|
||||
RUN.contains.push(
|
||||
hljs.BACKSLASH_ESCAPE,
|
||||
NAMED_ARG,
|
||||
hljs.QUOTE_STRING_MODE,
|
||||
NUMBER,
|
||||
MACRO,
|
||||
CLOSURE,
|
||||
);
|
||||
LET.contains.push(
|
||||
hljs.BACKSLASH_ESCAPE,
|
||||
NAMED_ARG,
|
||||
NUMBER,
|
||||
MACRO,
|
||||
CLOSURE,
|
||||
hljs.QUOTE_STRING_MODE,
|
||||
);
|
||||
SETVAR.contains.push(
|
||||
hljs.BACKSLASH_ESCAPE,
|
||||
NAMED_ARG,
|
||||
NUMBER,
|
||||
MACRO,
|
||||
CLOSURE,
|
||||
hljs.QUOTE_STRING_MODE,
|
||||
);
|
||||
GETVAR.contains.push(
|
||||
hljs.BACKSLASH_ESCAPE,
|
||||
NAMED_ARG,
|
||||
hljs.QUOTE_STRING_MODE,
|
||||
NUMBER,
|
||||
MACRO,
|
||||
CLOSURE,
|
||||
);
|
||||
COMMAND.contains.push(
|
||||
hljs.BACKSLASH_ESCAPE,
|
||||
NAMED_ARG,
|
||||
NUMBER,
|
||||
MACRO,
|
||||
CLOSURE,
|
||||
hljs.QUOTE_STRING_MODE,
|
||||
);
|
||||
CLOSURE.contains.push(
|
||||
hljs.BACKSLASH_ESCAPE,
|
||||
COMMENT,
|
||||
ABORT,
|
||||
NAMED_ARG,
|
||||
NUMBER,
|
||||
MACRO,
|
||||
RUN,
|
||||
LET,
|
||||
GETVAR,
|
||||
SETVAR,
|
||||
COMMAND,
|
||||
'self',
|
||||
hljs.QUOTE_STRING_MODE,
|
||||
);
|
||||
hljs.registerLanguage('stscript', ()=>({
|
||||
case_insensitive: false,
|
||||
keywords: ['|'],
|
||||
contains: [
|
||||
hljs.BACKSLASH_ESCAPE,
|
||||
COMMENT,
|
||||
ABORT,
|
||||
RUN,
|
||||
LET,
|
||||
GETVAR,
|
||||
SETVAR,
|
||||
COMMAND,
|
||||
CLOSURE,
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
getHelpString() {
|
||||
return '<div class="slashHelp">Loading...</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} text The text to parse.
|
||||
* @param {*} index Index to check for names (cursor position).
|
||||
*/
|
||||
async getNameAt(text, index) {
|
||||
if (this.text != text) {
|
||||
try {
|
||||
this.parse(text, false);
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
const executor = this.commandIndex
|
||||
.filter(it=>it.start <= index && (it.end >= index || it.end == null))
|
||||
.slice(-1)[0]
|
||||
?? null
|
||||
;
|
||||
|
||||
if (executor) {
|
||||
const childClosure = this.closureIndex
|
||||
.find(it=>it.start <= index && (it.end >= index || it.end == null) && it.start > executor.start)
|
||||
?? null
|
||||
;
|
||||
if (childClosure !== null) return null;
|
||||
const macro = this.macroIndex.findLast(it=>it.start <= index && it.end >= index);
|
||||
console.log(macro);
|
||||
if (macro) {
|
||||
const frag = document.createRange().createContextualFragment(await (await fetch('/scripts/templates/macros.html')).text());
|
||||
const options = [...frag.querySelectorAll('ul:nth-of-type(2n+1) > li')].map(li=>new MacroAutoCompleteOption(
|
||||
li.querySelector('tt').textContent.slice(2, -2).replace(/^([^\s:]+[\s:]+).*$/, '$1'),
|
||||
li.querySelector('tt').textContent,
|
||||
(li.querySelector('tt').remove(),li.innerHTML),
|
||||
));
|
||||
const result = new AutoCompleteNameResult(
|
||||
macro.name,
|
||||
macro.start + 2,
|
||||
options,
|
||||
false,
|
||||
()=>`No matching macros for "{{${result.name}}}"`,
|
||||
()=>'No macros found.',
|
||||
);
|
||||
return result;
|
||||
}
|
||||
if (executor.name == ':') {
|
||||
const options = this.scopeIndex[this.commandIndex.indexOf(executor)]
|
||||
?.allVariableNames
|
||||
?.map(it=>new SlashCommandVariableAutoCompleteOption(it))
|
||||
?? []
|
||||
;
|
||||
try {
|
||||
const qrApi = (await import('../extensions/quick-reply/index.js')).quickReplyApi;
|
||||
options.push(...qrApi.listSets()
|
||||
.map(set=>qrApi.listQuickReplies(set).map(qr=>`${set}.${qr}`))
|
||||
.flat()
|
||||
.map(qr=>new SlashCommandQuickReplyAutoCompleteOption(qr)),
|
||||
);
|
||||
} catch { /* empty */ }
|
||||
const result = new AutoCompleteNameResult(
|
||||
executor.unnamedArgumentList[0]?.value.toString(),
|
||||
executor.start,
|
||||
options,
|
||||
true,
|
||||
()=>`No matching variables in scope and no matching Quick Replies for "${result.name}"`,
|
||||
()=>'No variables in scope and no Quick Replies found.',
|
||||
);
|
||||
return result;
|
||||
}
|
||||
const result = new SlashCommandAutoCompleteNameResult(executor, this.commands);
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the index <length> number of characters forward and returns the last character taken.
|
||||
* @param {number} length Number of characters to take.
|
||||
* @param {boolean} keep Whether to add the characters to the kept text.
|
||||
* @returns The last character taken.
|
||||
*/
|
||||
take(length = 1) {
|
||||
this.jumpedEscapeSequence = false;
|
||||
let content = this.char;
|
||||
this.index++;
|
||||
if (length > 1) {
|
||||
content = this.take(length - 1);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
discardWhitespace() {
|
||||
while (/\s/.test(this.char)) {
|
||||
this.take(); // discard whitespace
|
||||
this.jumpedEscapeSequence = false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Tests if the next characters match a symbol.
|
||||
* Moves the index forward if the next characters are backslashes directly followed by the symbol.
|
||||
* Expects that the current char is taken after testing.
|
||||
* @param {string|RegExp} sequence Sequence of chars or regex character group that is the symbol.
|
||||
* @param {number} offset Offset from the current index (won't move the index if offset != 0).
|
||||
* @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
|
||||
// /echo abc \| /echo def
|
||||
// -> TOAST: abc | /echo def
|
||||
// /echo abc \\| /echo def
|
||||
// -> TOAST: abc \
|
||||
// -> TOAST: def
|
||||
// /echo abc \\\| /echo def
|
||||
// -> TOAST: abc \| /echo def
|
||||
// /echo abc \\\\| /echo def
|
||||
// -> TOAST: abc \\
|
||||
// -> TOAST: def
|
||||
// /echo title=\:} \{: | /echo title=\{: \:}
|
||||
// -> TOAST: *:}* {:
|
||||
// -> TOAST: *{:* :}
|
||||
const escapeOffset = this.jumpedEscapeSequence ? -1 : 0;
|
||||
const escapes = this.text.slice(this.index + offset + escapeOffset).replace(/^(\\*).*$/s, '$1').length;
|
||||
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;
|
||||
// uneven number of backslashes before sequence
|
||||
// = the final backslash escapes the sequence
|
||||
// = every preceding pair is one literal backslash
|
||||
// -> move index forward to skip the backslash escaping the first backslash or the symbol
|
||||
// even number of backslashes before sequence
|
||||
// = every pair is one literal backslash
|
||||
// -> move index forward to skip the backslash escaping the first backslash
|
||||
if (!this.jumpedEscapeSequence && offset == 0) {
|
||||
this.index++;
|
||||
this.jumpedEscapeSequence = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
replaceGetvar(value) {
|
||||
return value.replace(/{{(get(?:global)?var)::([^}]+)}}/gi, (_, cmd, name) => {
|
||||
name = name.trim();
|
||||
// store pipe
|
||||
const pipeName = `_PARSER_${uuidv4()}`;
|
||||
const storePipe = new SlashCommandExecutor(null); {
|
||||
storePipe.command = this.commands['let'];
|
||||
storePipe.name = 'let';
|
||||
const nameAss = new SlashCommandUnnamedArgumentAssignment();
|
||||
nameAss.value = pipeName;
|
||||
const valAss = new SlashCommandUnnamedArgumentAssignment();
|
||||
valAss.value = '{{pipe}}';
|
||||
storePipe.unnamedArgumentList = [nameAss, valAss];
|
||||
this.closure.executorList.push(storePipe);
|
||||
}
|
||||
// getvar / getglobalvar
|
||||
const getvar = new SlashCommandExecutor(null); {
|
||||
getvar.command = this.commands[cmd];
|
||||
getvar.name = 'cmd';
|
||||
const nameAss = new SlashCommandUnnamedArgumentAssignment();
|
||||
nameAss.value = name;
|
||||
getvar.unnamedArgumentList = [nameAss];
|
||||
this.closure.executorList.push(getvar);
|
||||
}
|
||||
// set to temp scoped var
|
||||
const varName = `_PARSER_${uuidv4()}`;
|
||||
const setvar = new SlashCommandExecutor(null); {
|
||||
setvar.command = this.commands['let'];
|
||||
setvar.name = 'let';
|
||||
const nameAss = new SlashCommandUnnamedArgumentAssignment();
|
||||
nameAss.value = varName;
|
||||
const valAss = new SlashCommandUnnamedArgumentAssignment();
|
||||
valAss.value = '{{pipe}}';
|
||||
setvar.unnamedArgumentList = [nameAss, valAss];
|
||||
this.closure.executorList.push(setvar);
|
||||
}
|
||||
// return pipe
|
||||
const returnPipe = new SlashCommandExecutor(null); {
|
||||
returnPipe.command = this.commands['return'];
|
||||
returnPipe.name = 'return';
|
||||
const varAss = new SlashCommandUnnamedArgumentAssignment();
|
||||
varAss.value = `{{var::${pipeName}}}`;
|
||||
returnPipe.unnamedArgumentList = [varAss];
|
||||
this.closure.executorList.push(returnPipe);
|
||||
}
|
||||
return `{{var::${varName}}}`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
parse(text, verifyCommandNames = true, flags = null, abortController = null) {
|
||||
this.verifyCommandNames = verifyCommandNames;
|
||||
for (const key of Object.keys(PARSER_FLAG)) {
|
||||
this.flags[PARSER_FLAG[key]] = flags?.[PARSER_FLAG[key]] ?? power_user.stscript.parser.flags[PARSER_FLAG[key]] ?? false;
|
||||
}
|
||||
this.abortController = abortController;
|
||||
this.text = text;
|
||||
this.index = 0;
|
||||
this.scope = null;
|
||||
this.closureIndex = [];
|
||||
this.commandIndex = [];
|
||||
this.scopeIndex = [];
|
||||
this.macroIndex = [];
|
||||
const closure = this.parseClosure(true);
|
||||
return closure;
|
||||
}
|
||||
|
||||
testClosure() {
|
||||
return this.testSymbol('{:');
|
||||
}
|
||||
testClosureEnd() {
|
||||
if (!this.scope.parent) {
|
||||
// "root" closure does not have {: and :}
|
||||
if (this.index >= this.text.length) return true;
|
||||
return false;
|
||||
}
|
||||
if (!this.verifyCommandNames) {
|
||||
if (this.index >= this.text.length) return true;
|
||||
} else {
|
||||
if (this.ahead.length < 1) throw new SlashCommandParserError(`Unclosed closure at position ${this.userIndex}`, this.text, this.index);
|
||||
}
|
||||
return this.testSymbol(':}');
|
||||
}
|
||||
parseClosure(isRoot = false) {
|
||||
const closureIndexEntry = { start:this.index + 1, end:null };
|
||||
this.closureIndex.push(closureIndexEntry);
|
||||
let injectPipe = true;
|
||||
if (!isRoot) this.take(2); // discard opening {:
|
||||
let closure = new SlashCommandClosure(this.scope);
|
||||
closure.abortController = this.abortController;
|
||||
this.scope = closure.scope;
|
||||
this.closure = closure;
|
||||
this.discardWhitespace();
|
||||
while (this.testNamedArgument()) {
|
||||
const arg = this.parseNamedArgument();
|
||||
closure.argumentList.push(arg);
|
||||
this.scope.variableNames.push(arg.name);
|
||||
this.discardWhitespace();
|
||||
}
|
||||
while (!this.testClosureEnd()) {
|
||||
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;
|
||||
} else if (this.testCommand()) {
|
||||
const cmd = this.parseCommand();
|
||||
cmd.injectPipe = injectPipe;
|
||||
closure.executorList.push(cmd);
|
||||
injectPipe = true;
|
||||
} else {
|
||||
while (!this.testCommandEnd()) this.take(); // discard plain text and comments
|
||||
}
|
||||
this.discardWhitespace();
|
||||
// first pipe marks end of command
|
||||
if (this.testSymbol('|')) {
|
||||
this.take(); // discard first pipe
|
||||
// second pipe indicates no pipe injection for the next command
|
||||
if (this.testSymbol('|')) {
|
||||
injectPipe = false;
|
||||
this.take(); // discard second pipe
|
||||
}
|
||||
}
|
||||
this.discardWhitespace(); // discard further whitespace
|
||||
}
|
||||
if (!isRoot) this.take(2); // discard closing :}
|
||||
if (this.testSymbol('()')) {
|
||||
this.take(2); // discard ()
|
||||
closure.executeNow = true;
|
||||
}
|
||||
closureIndexEntry.end = this.index - 1;
|
||||
this.discardWhitespace(); // discard trailing whitespace
|
||||
this.scope = closure.scope.parent;
|
||||
return closure;
|
||||
}
|
||||
|
||||
testComment() {
|
||||
return this.testSymbol(/\/[/#]/);
|
||||
}
|
||||
testCommentEnd() {
|
||||
return this.testCommandEnd();
|
||||
}
|
||||
parseComment() {
|
||||
const start = this.index + 1;
|
||||
const cmd = new SlashCommandExecutor(start);
|
||||
cmd.command = this.commands['/'];
|
||||
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.unnamedArgumentList = [];
|
||||
cmd.command = this.commands[cmd.name];
|
||||
this.commandIndex.push(cmd);
|
||||
this.scopeIndex.push(this.scope.getCopy());
|
||||
this.take(13); // discard "/parser-flag "
|
||||
cmd.startNamedArgs = -1;
|
||||
cmd.endNamedArgs = -1;
|
||||
cmd.startUnnamedArgs = this.index;
|
||||
cmd.unnamedArgumentList = this.parseUnnamedArgument(true);
|
||||
const [flag, state] = cmd.unnamedArgumentList ?? [null, null];
|
||||
cmd.endUnnamedArgs = this.index;
|
||||
if (Object.keys(PARSER_FLAG).includes(flag.value.toString())) {
|
||||
this.flags[PARSER_FLAG[flag.value.toString()]] = isTrueBoolean(state?.value.toString() ?? 'on');
|
||||
}
|
||||
cmd.end = this.index;
|
||||
}
|
||||
|
||||
testRunShorthand() {
|
||||
return this.testSymbol('/:') && !this.testSymbol(':}', 1);
|
||||
}
|
||||
testRunShorthandEnd() {
|
||||
return this.testCommandEnd();
|
||||
}
|
||||
parseRunShorthand() {
|
||||
const start = this.index + 2;
|
||||
const cmd = new SlashCommandExecutor(start);
|
||||
cmd.name = ':';
|
||||
cmd.unnamedArgumentList = [];
|
||||
cmd.command = this.commands['run'];
|
||||
this.commandIndex.push(cmd);
|
||||
this.scopeIndex.push(this.scope.getCopy());
|
||||
this.take(2); //discard "/:"
|
||||
const assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
if (this.testQuotedValue()) assignment.value = this.parseQuotedValue();
|
||||
else assignment.value = this.parseValue();
|
||||
cmd.unnamedArgumentList = [assignment];
|
||||
this.discardWhitespace();
|
||||
while (this.testNamedArgument()) {
|
||||
const arg = this.parseNamedArgument();
|
||||
cmd.namedArgumentList.push(arg);
|
||||
this.discardWhitespace();
|
||||
}
|
||||
this.discardWhitespace();
|
||||
// /run shorthand does not take unnamed arguments (the command name practically *is* the unnamed argument)
|
||||
if (this.testRunShorthandEnd()) {
|
||||
cmd.end = this.index;
|
||||
return cmd;
|
||||
} else {
|
||||
console.warn(this.behind, this.char, this.ahead);
|
||||
throw new SlashCommandParserError(`Unexpected end of command at position ${this.userIndex}: "/${cmd.name}"`, this.text, this.index);
|
||||
}
|
||||
}
|
||||
|
||||
testCommand() {
|
||||
return this.testSymbol('/');
|
||||
}
|
||||
testCommandEnd() {
|
||||
return this.testClosureEnd() || this.testSymbol('|');
|
||||
}
|
||||
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 "/"
|
||||
while (!/\s/.test(this.char) && !this.testCommandEnd()) cmd.name += this.take(); // take chars until whitespace or end
|
||||
this.discardWhitespace();
|
||||
if (this.verifyCommandNames && !this.commands[cmd.name]) throw new SlashCommandParserError(`Unknown command at position ${this.index - cmd.name.length}: "/${cmd.name}"`, this.text, this.index - cmd.name.length);
|
||||
cmd.command = this.commands[cmd.name];
|
||||
cmd.startNamedArgs = this.index;
|
||||
cmd.endNamedArgs = this.index;
|
||||
while (this.testNamedArgument()) {
|
||||
const arg = this.parseNamedArgument();
|
||||
cmd.namedArgumentList.push(arg);
|
||||
cmd.endNamedArgs = this.index;
|
||||
this.discardWhitespace();
|
||||
}
|
||||
this.discardWhitespace();
|
||||
cmd.startUnnamedArgs = this.index;
|
||||
cmd.endUnnamedArgs = this.index;
|
||||
if (this.testUnnamedArgument()) {
|
||||
cmd.unnamedArgumentList = this.parseUnnamedArgument(cmd.command?.unnamedArgumentList?.length && cmd?.command?.splitUnnamedArgument);
|
||||
cmd.endUnnamedArgs = this.index;
|
||||
if (cmd.name == 'let') {
|
||||
const keyArg = cmd.namedArgumentList.find(it=>it.name == 'key');
|
||||
if (keyArg) {
|
||||
this.scope.variableNames.push(keyArg.value.toString());
|
||||
} else if (typeof cmd.unnamedArgumentList[0]?.value == 'string') {
|
||||
this.scope.variableNames.push(cmd.unnamedArgumentList[0].value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.testCommandEnd()) {
|
||||
cmd.end = this.index;
|
||||
return cmd;
|
||||
} else {
|
||||
console.warn(this.behind, this.char, this.ahead);
|
||||
throw new SlashCommandParserError(`Unexpected end of command at position ${this.userIndex}: "/${cmd.name}"`, this.text, this.index);
|
||||
}
|
||||
}
|
||||
|
||||
testNamedArgument() {
|
||||
return /^(\w+)=/.test(`${this.char}${this.ahead}`);
|
||||
}
|
||||
parseNamedArgument() {
|
||||
let assignment = new SlashCommandNamedArgumentAssignment();
|
||||
assignment.start = this.index;
|
||||
let key = '';
|
||||
while (/\w/.test(this.char)) key += this.take(); // take chars
|
||||
this.take(); // discard "="
|
||||
assignment.name = key;
|
||||
if (this.testClosure()) {
|
||||
assignment.value = this.parseClosure();
|
||||
} else if (this.testQuotedValue()) {
|
||||
assignment.value = this.parseQuotedValue();
|
||||
} else if (this.testListValue()) {
|
||||
assignment.value = this.parseListValue();
|
||||
} else if (this.testValue()) {
|
||||
assignment.value = this.parseValue();
|
||||
}
|
||||
assignment.end = this.index;
|
||||
return assignment;
|
||||
}
|
||||
|
||||
testUnnamedArgument() {
|
||||
return !this.testCommandEnd();
|
||||
}
|
||||
testUnnamedArgumentEnd() {
|
||||
return this.testCommandEnd();
|
||||
}
|
||||
parseUnnamedArgument(split) {
|
||||
/**@type {SlashCommandClosure|String}*/
|
||||
let value = this.jumpedEscapeSequence ? this.take() : ''; // take the first, already tested, char if it is an escaped one
|
||||
let isList = split;
|
||||
let listValues = [];
|
||||
/**@type {SlashCommandUnnamedArgumentAssignment}*/
|
||||
let assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
assignment.start = this.index;
|
||||
while (!this.testUnnamedArgumentEnd()) {
|
||||
if (this.testClosure()) {
|
||||
isList = true;
|
||||
if (value.length > 0) {
|
||||
assignment.end = assignment.end - (value.length - value.trim().length);
|
||||
this.indexMacros(this.index - value.length, value);
|
||||
assignment.value = value.trim();
|
||||
listValues.push(assignment);
|
||||
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
assignment.start = this.index;
|
||||
value = '';
|
||||
}
|
||||
assignment.value = this.parseClosure();
|
||||
assignment.end = this.index;
|
||||
listValues.push(assignment);
|
||||
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
assignment.start = this.index;
|
||||
} else if (split) {
|
||||
if (this.testQuotedValue()) {
|
||||
assignment.start = this.index;
|
||||
assignment.value = this.parseQuotedValue();
|
||||
assignment.end = this.index;
|
||||
listValues.push(assignment);
|
||||
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
} else if (this.testListValue()) {
|
||||
assignment.start = this.index;
|
||||
assignment.value = this.parseListValue();
|
||||
assignment.end = this.index;
|
||||
listValues.push(assignment);
|
||||
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
} else if (this.testValue()) {
|
||||
assignment.start = this.index;
|
||||
assignment.value = this.parseValue();
|
||||
assignment.end = this.index;
|
||||
listValues.push(assignment);
|
||||
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
} else {
|
||||
throw new SlashCommandParserError(`Unexpected end of unnamed argument at index ${this.userIndex}.`);
|
||||
}
|
||||
this.discardWhitespace();
|
||||
} else {
|
||||
value += this.take();
|
||||
assignment.end = this.index;
|
||||
}
|
||||
}
|
||||
if (isList && value.trim().length > 0) {
|
||||
assignment.value = value.trim();
|
||||
listValues.push(assignment);
|
||||
}
|
||||
if (isList) {
|
||||
return listValues;
|
||||
}
|
||||
this.indexMacros(this.index - value.length, value);
|
||||
value = value.trim();
|
||||
if (this.flags[PARSER_FLAG.REPLACE_GETVAR]) {
|
||||
value = this.replaceGetvar(value);
|
||||
}
|
||||
assignment.value = value;
|
||||
return [assignment];
|
||||
}
|
||||
|
||||
testQuotedValue() {
|
||||
return this.testSymbol('"');
|
||||
}
|
||||
testQuotedValueEnd() {
|
||||
if (this.endOfText) {
|
||||
if (this.verifyCommandNames) throw new SlashCommandParserError(`Unexpected end of quoted value at position ${this.index}`, this.text, this.index);
|
||||
else return true;
|
||||
}
|
||||
if (!this.verifyCommandNames && this.testClosureEnd()) return true;
|
||||
if (this.verifyCommandNames && !this.flags[PARSER_FLAG.STRICT_ESCAPING] && this.testCommandEnd()) {
|
||||
throw new SlashCommandParserError(`Unexpected end of quoted value at position ${this.index}`, this.text, this.index);
|
||||
}
|
||||
return this.testSymbol('"') || (!this.flags[PARSER_FLAG.STRICT_ESCAPING] && this.testCommandEnd());
|
||||
}
|
||||
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
|
||||
if (this.flags[PARSER_FLAG.REPLACE_GETVAR]) {
|
||||
value = this.replaceGetvar(value);
|
||||
}
|
||||
this.indexMacros(this.index - value.length, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
testListValue() {
|
||||
return this.testSymbol('[');
|
||||
}
|
||||
testListValueEnd() {
|
||||
if (this.endOfText) throw new SlashCommandParserError(`Unexpected end of list value at position ${this.index}`, this.text, this.index);
|
||||
return this.testSymbol(']');
|
||||
}
|
||||
parseListValue() {
|
||||
let value = this.take(); // take the already tested opening bracket
|
||||
while (!this.testListValueEnd()) value += this.take(); // take all chars until closing bracket
|
||||
value += this.take(); // take closing bracket
|
||||
if (this.flags[PARSER_FLAG.REPLACE_GETVAR]) {
|
||||
value = this.replaceGetvar(value);
|
||||
}
|
||||
this.indexMacros(this.index - value.length, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
testValue() {
|
||||
return !this.testSymbol(/\s/);
|
||||
}
|
||||
testValueEnd() {
|
||||
if (this.testSymbol(/\s/)) return true;
|
||||
return this.testCommandEnd();
|
||||
}
|
||||
parseValue() {
|
||||
let value = this.jumpedEscapeSequence ? this.take() : ''; // take the first, already tested, char if it is an escaped one
|
||||
while (!this.testValueEnd()) value += this.take(); // take all chars until value end
|
||||
if (this.flags[PARSER_FLAG.REPLACE_GETVAR]) {
|
||||
value = this.replaceGetvar(value);
|
||||
}
|
||||
this.indexMacros(this.index - value.length, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
indexMacros(offset, text) {
|
||||
const re = /{{(?:((?:(?!}})[^\s:])+[\s:]*)((?:(?!}}).)*)(}}|}$|$))?/s;
|
||||
let remaining = text;
|
||||
let localOffset = 0;
|
||||
while (remaining.length > 0 && re.test(remaining)) {
|
||||
const match = re.exec(remaining);
|
||||
this.macroIndex.push({
|
||||
start: offset + localOffset + match.index,
|
||||
end: offset + localOffset + match.index + (match[0]?.length ?? 0),
|
||||
name: match[1] ?? '',
|
||||
});
|
||||
localOffset += match.index + (match[0]?.length ?? 0);
|
||||
remaining = remaining.slice(match.index + (match[0]?.length ?? 0));
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user