| import { hljs } from '../../lib.js'; |
| import { power_user } from '../power-user.js'; |
| import { isFalseBoolean, 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'; |
| import { SlashCommandScope } from './SlashCommandScope.js'; |
| import { SlashCommandVariableAutoCompleteOption } from './SlashCommandVariableAutoCompleteOption.js'; |
| import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js'; |
| import { SlashCommandAbortController } from './SlashCommandAbortController.js'; |
| import { SlashCommandAutoCompleteNameResult } from './SlashCommandAutoCompleteNameResult.js'; |
| import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js'; |
| import { SlashCommandEnumValue } from './SlashCommandEnumValue.js'; |
| import { EnhancedMacroAutoCompleteOption, parseMacroContext } from '../autocomplete/EnhancedMacroAutoCompleteOption.js'; |
| import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js'; |
| import { SlashCommandDebugController } from './SlashCommandDebugController.js'; |
| import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js'; |
| import { SlashCommandBreak } from './SlashCommandBreak.js'; |
| import { macros as macroSystem } from '../macros/macro-system.js'; |
|
|
| |
| |
|
|
| |
| |
| |
| |
| |
| export const PARSER_FLAG = { |
| 'STRICT_ESCAPING': 1, |
| 'REPLACE_GETVAR': 2, |
| }; |
|
|
| export class SlashCommandParser { |
| static commands = {}; |
|
|
| |
| |
| |
| |
| |
| |
| |
| static addCommand(command, callback, aliases, helpString = '') { |
| this.addCommandObject(SlashCommand.fromProps({ |
| name: command, |
| callback, |
| aliases, |
| helpString, |
| })); |
| } |
| |
| |
| |
| |
| static addCommandObject(command) { |
| const reserved = ['/', '#', ':', 'parser-flag', 'breakpoint']; |
| 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); |
| } |
| |
| |
| |
| |
| 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]); |
| } |
|
|
| const stack = new Error().stack.split('\n').map(it=>it.trim()); |
| command.isExtension = stack.find(it=>it.includes('/scripts/extensions/')) != null; |
| command.isThirdParty = stack.find(it=>it.includes('/scripts/extensions/third-party/')) != null; |
| if (command.isThirdParty) { |
| command.source = stack.find(it=>it.includes('/scripts/extensions/third-party/')).replace(/^.*?\/scripts\/extensions\/third-party\/([^/]+)\/.*$/, '$1'); |
| } else if (command.isExtension) { |
| command.source = stack.find(it=>it.includes('/scripts/extensions/')).replace(/^.*?\/scripts\/extensions\/([^/]+)\/.*$/, '$1'); |
| } else { |
| const idx = stack.findLastIndex(it=>it.includes('at SlashCommandParser.')) + 1; |
| command.source = stack[idx].replace(/^.*?\/((?:scripts\/)?(?:[^/]+)\.js).*$/, '$1'); |
| } |
|
|
| this.commands[command.name] = command; |
|
|
| if (Array.isArray(command.aliases)) { |
| command.aliases.forEach((alias) => { |
| this.commands[alias] = command; |
| }); |
| } |
| } |
|
|
|
|
| get commands() { |
| return SlashCommandParser.commands; |
| } |
| helpStrings = {}; |
| verifyCommandNames = true; |
| text; |
| index; |
| abortController; |
| debugController; |
| scope; |
| closure; |
|
|
| flags = {}; |
|
|
| jumpedEscapeSequence = false; |
|
|
| closureIndex; |
| macroIndex; |
| commandIndex; |
| scopeIndex; |
|
|
| parserContext; |
|
|
| 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.char) && /^\s+$/.test(this.ahead)); |
| } |
|
|
|
|
| constructor() { |
| |
| 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: commonEnumProviders.boolean('onOff')(), |
| }), |
| ], |
| 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.', |
| })); |
| } |
| if (!Object.keys(this.commands).includes('breakpoint')) { |
| SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: 'breakpoint', |
| helpString: 'Set a breakpoint for debugging in the QR Editor.', |
| })); |
| } |
| if (!Object.keys(this.commands).includes('break')) { |
| SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: 'break', |
| helpString: 'Break out of a loop or closure executed through /run or /:', |
| unnamedArgumentList: [ |
| SlashCommandArgument.fromProps({ description: 'value to pass down the pipe instead of the current pipe value', |
| typeList: Object.values(ARGUMENT_TYPE), |
| }), |
| ], |
| })); |
| } |
|
|
| |
| this.registerLanguage(); |
| } |
| registerLanguage() { |
| |
| |
| const decimalDigits = '[0-9](_?[0-9])*'; |
| const frac = `\\.(${decimalDigits})`; |
| |
| |
| const decimalInteger = '0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*'; |
| const NUMBER = { |
| className: 'number', |
| variants: [ |
| |
| { begin: `(\\b(${decimalInteger})((${frac})|\\.)?|(${frac}))` + |
| `[eE][+-]?(${decimalDigits})\\b` }, |
| { begin: `\\b(${decimalInteger})\\b((${frac})\\b|\\.)?|(${frac})\\b` }, |
|
|
| |
| { begin: '\\b(0|[1-9](_?[0-9])*)n\\b' }, |
|
|
| |
| { 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' }, |
|
|
| |
| |
| { begin: '\\b0[0-7]+n?\\b' }, |
| ], |
| relevance: 0, |
| }; |
|
|
| function getQuotedRunRegex() { |
| try { |
| return new RegExp('(".+?(?<!\\\\)")|((?:[^\\s\\|"]|"[^"]*")*)(\\||$|\\s)'); |
| } catch { |
| |
| return /(".+?")|(\S+?)(\||$|\s)/; |
| } |
| } |
|
|
| const BLOCK_COMMENT = { |
| scope: 'comment', |
| begin: /\/\*/, |
| end: /\*\|/, |
| contains: [], |
| }; |
| const COMMENT = { |
| scope: 'comment', |
| begin: /\/[/#]/, |
| end: /\||$|:}/, |
| contains: [], |
| }; |
| const ABORT = { |
| begin: /\/(abort|breakpoint)/, |
| beginScope: 'abort', |
| end: /\||$|(?=:})/, |
| excludeEnd: false, |
| returnEnd: true, |
| contains: [], |
| }; |
| const IMPORT = { |
| scope: 'command', |
| begin: /\/(import)/, |
| beginScope: 'keyword', |
| end: /\||$|(?=:})/, |
| excludeEnd: false, |
| returnEnd: true, |
| contains: [], |
| }; |
| const BREAK = { |
| scope: 'command', |
| begin: /\/(break)/, |
| beginScope: 'keyword', |
| end: /\||$|(?=:})/, |
| excludeEnd: false, |
| returnEnd: true, |
| contains: [], |
| }; |
| const LET = { |
| begin: [ |
| /\/(let|var)\s+/, |
| ], |
| beginScope: { |
| 1: 'variable', |
| }, |
| end: /\||$|:}/, |
| excludeEnd: false, |
| returnEnd: true, |
| contains: [], |
| }; |
| const SETVAR = { |
| begin: /\/(setvar|setglobalvar)\s+/, |
| beginScope: 'variable', |
| end: /\||$|:}/, |
| excludeEnd: false, |
| returnEnd: true, |
| contains: [], |
| }; |
| const GETVAR = { |
| begin: /\/(getvar|getglobalvar)\s+/, |
| beginScope: 'variable', |
| end: /\||$|:}/, |
| excludeEnd: false, |
| returnEnd: true, |
| contains: [], |
| }; |
| const RUN = { |
| match: [ |
| /\/:/, |
| getQuotedRunRegex(), |
| /\||$|(?=:})/, |
| ], |
| className: { |
| 1: 'variable.language', |
| 2: 'title.function.invoke', |
| }, |
| contains: [], |
| }; |
| const COMMAND = { |
| scope: 'command', |
| begin: /\/\S+/, |
| beginScope: 'title.function', |
| end: /\||$|(?=:})/, |
| excludeEnd: false, |
| returnEnd: true, |
| contains: [], |
| }; |
| const CLOSURE = { |
| scope: 'closure', |
| begin: /{:/, |
| end: /:}(\(\))?/, |
| beginScope: 'punctuation', |
| endScope: 'punctuation', |
| contains: [], |
| }; |
| const NAMED_ARG = { |
| scope: 'property', |
| begin: /\w+=/, |
| end: '', |
| }; |
| const MACRO = { |
| scope: 'variable', |
| begin: /{{/, |
| end: /}}/, |
| }; |
| const PIPEBREAK = { |
| beginScope: 'pipebreak', |
| begin: /\|\|/, |
| end: '', |
| }; |
| const PIPE = { |
| beginScope: 'pipe', |
| begin: /\|/, |
| end: '', |
| }; |
| BLOCK_COMMENT.contains.push( |
| BLOCK_COMMENT, |
| ); |
| RUN.contains.push( |
| hljs.BACKSLASH_ESCAPE, |
| NAMED_ARG, |
| hljs.QUOTE_STRING_MODE, |
| NUMBER, |
| MACRO, |
| CLOSURE, |
| ); |
| IMPORT.contains.push( |
| hljs.BACKSLASH_ESCAPE, |
| NAMED_ARG, |
| NUMBER, |
| MACRO, |
| CLOSURE, |
| hljs.QUOTE_STRING_MODE, |
| ); |
| BREAK.contains.push( |
| hljs.BACKSLASH_ESCAPE, |
| NAMED_ARG, |
| NUMBER, |
| MACRO, |
| CLOSURE, |
| hljs.QUOTE_STRING_MODE, |
| ); |
| 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, |
| ); |
| ABORT.contains.push( |
| hljs.BACKSLASH_ESCAPE, |
| NAMED_ARG, |
| NUMBER, |
| MACRO, |
| CLOSURE, |
| hljs.QUOTE_STRING_MODE, |
| ); |
| COMMAND.contains.push( |
| hljs.BACKSLASH_ESCAPE, |
| NAMED_ARG, |
| NUMBER, |
| MACRO, |
| CLOSURE, |
| hljs.QUOTE_STRING_MODE, |
| ); |
| CLOSURE.contains.push( |
| hljs.BACKSLASH_ESCAPE, |
| BLOCK_COMMENT, |
| COMMENT, |
| ABORT, |
| IMPORT, |
| BREAK, |
| NAMED_ARG, |
| NUMBER, |
| MACRO, |
| RUN, |
| LET, |
| GETVAR, |
| SETVAR, |
| COMMAND, |
| 'self', |
| hljs.QUOTE_STRING_MODE, |
| PIPEBREAK, |
| PIPE, |
| ); |
| hljs.registerLanguage('stscript', ()=>({ |
| case_insensitive: false, |
| keywords: [], |
| contains: [ |
| hljs.BACKSLASH_ESCAPE, |
| BLOCK_COMMENT, |
| COMMENT, |
| ABORT, |
| IMPORT, |
| BREAK, |
| RUN, |
| LET, |
| GETVAR, |
| SETVAR, |
| COMMAND, |
| CLOSURE, |
| PIPEBREAK, |
| PIPE, |
| ], |
| })); |
| } |
|
|
| getHelpString() { |
| return '<div class="slashHelp">Loading...</div>'; |
| } |
|
|
| |
| |
| |
| |
| |
| async getNameAt(text, index) { |
| if (this.text != text) { |
| try { |
| this.parse(text, false); |
| } catch (e) { |
| |
| 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); |
| if (macro) { |
| |
| const cursorInMacro = index - macro.start - 2; |
| const macroContent = text.slice(macro.start + 2, macro.end - (text.slice(macro.end - 2, macro.end) === '}}' ? 2 : 0)); |
| const context = parseMacroContext(macroContent, cursorInMacro); |
|
|
| |
| const identifier = macro.name.replace(/[\s:}]+$/, '').trim(); |
|
|
| |
| const options = this.#buildEnhancedMacroOptions(context); |
| const result = new AutoCompleteNameResult( |
| identifier, |
| macro.start + 2, |
| options, |
| false, |
| ); |
| return result; |
| } |
| if (executor.name == ':') { |
| const options = this.scopeIndex[this.commandIndex.indexOf(executor)] |
| ?.allVariableNames |
| ?.map(it=>new SlashCommandVariableAutoCompleteOption(it)) |
| ?? [] |
| ; |
| try { |
| if ('quickReplyApi' in globalThis) { |
| const qrApi = globalThis.quickReplyApi; |
| options.push(...qrApi.listSets() |
| .map(set=>qrApi.listQuickReplies(set).map(qr=>`${set}.${qr}`)) |
| .flat() |
| .map(qr=>new SlashCommandQuickReplyAutoCompleteOption(qr)), |
| ); |
| } |
| } catch { } |
| 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.scopeIndex[this.commandIndex.indexOf(executor)], this.commands); |
| return result; |
| } |
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| #buildEnhancedMacroOptions(context) { |
| |
| const options = []; |
|
|
| |
| const allMacros = macroSystem.registry.getAllMacros({ excludeHiddenAliases: true }); |
|
|
| |
| const isTypingArgs = context.currentArgIndex >= 0; |
|
|
| for (const macro of allMacros) { |
| |
| const isExactMatch = macro.name === context.identifier; |
| const isAliasMatch = macro.aliasOf === context.identifier; |
|
|
| |
| |
| const macroContext = (isExactMatch || isAliasMatch) ? context : null; |
|
|
| const option = new EnhancedMacroAutoCompleteOption(macro, macroContext); |
|
|
| |
| if (isTypingArgs && (isExactMatch || isAliasMatch)) { |
| options.unshift(option); |
| } else { |
| options.push(option); |
| } |
| } |
|
|
| return options; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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(); |
| this.jumpedEscapeSequence = false; |
| } |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| testSymbol(sequence, offset = 0) { |
| if (!this.flags[PARSER_FLAG.STRICT_ESCAPING]) return this.testSymbolLooseyGoosey(sequence, offset); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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))) { |
| |
| |
| if (escapes == 0) return true; |
| |
| |
| |
| |
| |
| |
| |
| 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))) { |
| |
| |
| if (escapes == 0) return true; |
| |
| |
| if (!this.jumpedEscapeSequence && offset == 0) { |
| this.index++; |
| this.jumpedEscapeSequence = true; |
| } |
| return false; |
| } |
| } |
|
|
| replaceGetvar(value) { |
| return value.replace(/{{(get(?:global)?var)::([^}]+)}}/gi, (match, cmd, name, idx) => { |
| name = name.trim(); |
| cmd = cmd.toLowerCase(); |
| const startIdx = this.index - value.length + idx; |
| const endIdx = this.index - value.length + idx + match.length; |
| |
| const pipeName = `_PARSER_PIPE_${uuidv4()}`; |
| const storePipe = new SlashCommandExecutor(startIdx); { |
| storePipe.end = endIdx; |
| 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); |
| } |
| |
| const getvar = new SlashCommandExecutor(startIdx); { |
| getvar.end = endIdx; |
| getvar.command = this.commands[cmd]; |
| getvar.name = cmd; |
| const nameAss = new SlashCommandUnnamedArgumentAssignment(); |
| nameAss.value = name; |
| getvar.unnamedArgumentList = [nameAss]; |
| this.closure.executorList.push(getvar); |
| } |
| |
| const varName = `_PARSER_VAR_${uuidv4()}`; |
| const setvar = new SlashCommandExecutor(startIdx); { |
| setvar.end = endIdx; |
| 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); |
| } |
| |
| const returnPipe = new SlashCommandExecutor(startIdx); { |
| returnPipe.end = endIdx; |
| 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, debugController = 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.debugController = debugController; |
| this.text = text; |
| this.index = 0; |
| this.scope = null; |
| this.closureIndex = []; |
| this.commandIndex = []; |
| this.scopeIndex = []; |
| this.macroIndex = []; |
| this.parserContext = uuidv4(); |
| const closure = this.parseClosure(true); |
| return closure; |
| } |
|
|
| testClosure() { |
| return this.testSymbol('{:'); |
| } |
| testClosureEnd() { |
| if (!this.scope.parent) { |
| |
| 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); |
| const textStart = this.index; |
| let closure = new SlashCommandClosure(this.scope); |
| closure.parserContext = this.parserContext; |
| closure.fullText = this.text; |
| closure.abortController = this.abortController; |
| closure.debugController = this.debugController; |
| this.scope = closure.scope; |
| const oldClosure = this.closure; |
| 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.testBlockComment()) { |
| this.parseBlockComment(); |
| } else 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.testBreakPoint()) { |
| const bp = this.parseBreakPoint(); |
| if (this.debugController) { |
| closure.executorList.push(bp); |
| } |
| } else if (this.testBreak()) { |
| const b = this.parseBreak(); |
| closure.executorList.push(b); |
| } else if (this.testCommand()) { |
| const cmd = this.parseCommand(); |
| cmd.injectPipe = injectPipe; |
| closure.executorList.push(cmd); |
| injectPipe = true; |
| } else { |
| while (!this.testCommandEnd()) this.take(); |
| } |
| this.discardWhitespace(); |
| |
| if (this.testSymbol('|')) { |
| this.take(); |
| |
| if (this.testSymbol('|')) { |
| injectPipe = false; |
| this.take(); |
| } |
| } |
| this.discardWhitespace(); |
| } |
| closure.rawText = this.text.slice(textStart, this.index); |
| if (!isRoot) this.take(2); |
| if (this.testSymbol('()')) { |
| this.take(2); |
| closure.executeNow = true; |
| } |
| closureIndexEntry.end = this.index - 1; |
| this.scope = closure.scope.parent; |
| this.closure = oldClosure ?? closure; |
| return closure; |
| } |
|
|
| testBreakPoint() { |
| return this.testSymbol(/\/breakpoint\s*\|/); |
| } |
| parseBreakPoint() { |
| const cmd = new SlashCommandBreakPoint(); |
| cmd.name = 'breakpoint'; |
| cmd.command = this.commands['breakpoint']; |
| cmd.start = this.index + 1; |
| this.take('/breakpoint'.length); |
| cmd.end = this.index; |
| this.commandIndex.push(cmd); |
| this.scopeIndex.push(this.scope.getCopy()); |
| return cmd; |
| } |
|
|
| testBreak() { |
| return this.testSymbol(/\/break(\s|\||$)/); |
| } |
| parseBreak() { |
| const cmd = new SlashCommandBreak(); |
| cmd.name = 'break'; |
| cmd.command = this.commands['break']; |
| cmd.start = this.index + 1; |
| this.take('/break'.length); |
| this.discardWhitespace(); |
| if (this.testUnnamedArgument()) { |
| cmd.unnamedArgumentList.push(...this.parseUnnamedArgument()); |
| } |
| cmd.end = this.index; |
| this.commandIndex.push(cmd); |
| this.scopeIndex.push(this.scope.getCopy()); |
| return cmd; |
| } |
|
|
| testBlockComment() { |
| return this.testSymbol('/*'); |
| } |
| testBlockCommentEnd() { |
| if (!this.verifyCommandNames) { |
| if (this.index >= this.text.length) return true; |
| } else { |
| if (this.ahead.length < 1) throw new SlashCommandParserError(`Unclosed block comment at position ${this.userIndex}`, this.text, this.index); |
| } |
| return this.testSymbol('*|'); |
| } |
| parseBlockComment() { |
| 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(); |
| cmd.name = this.take(); |
| while (!this.testBlockCommentEnd()) { |
| if (this.testBlockComment()) { |
| this.parseBlockComment(); |
| } |
| this.take(); |
| } |
| this.take(2); |
| cmd.end = this.index - 1; |
| } |
|
|
| testComment() { |
| return this.testSymbol(/\/[/#]/); |
| } |
| testCommentEnd() { |
| if (!this.verifyCommandNames) { |
| if (this.index >= this.text.length) return true; |
| } else { |
| if (this.endOfText) throw new SlashCommandParserError(`Unclosed comment at position ${this.userIndex}`, this.text, this.index); |
| } |
| return this.testSymbol('|'); |
| } |
| 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(); |
| cmd.name = this.take(); |
| 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); |
| 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); |
| const assignment = new SlashCommandUnnamedArgumentAssignment(); |
| if (this.testQuotedValue()) assignment.value = this.parseQuotedValue(); |
| else assignment.value = this.parseValue(); |
| cmd.unnamedArgumentList = [assignment]; |
| this.discardWhitespace(); |
| cmd.startNamedArgs = this.index; |
| while (this.testNamedArgument()) { |
| const arg = this.parseNamedArgument(); |
| cmd.namedArgumentList.push(arg); |
| this.discardWhitespace(); |
| } |
| cmd.endNamedArgs = this.index; |
| this.discardWhitespace(); |
| |
| 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(); |
| while (!/\s/.test(this.char) && !this.testCommandEnd()) cmd.name += this.take(); |
| 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 - (/\s(\s*)$/s.exec(this.behind)?.[1]?.length ?? 0); |
| cmd.endUnnamedArgs = this.index; |
| if (this.testUnnamedArgument()) { |
| const rawQuotesArg = cmd?.namedArgumentList?.find(a => a.name === 'raw'); |
| const rawQuotes = cmd?.command?.rawQuotes && rawQuotesArg ? !isFalseBoolean(rawQuotesArg?.value?.toString()) : cmd?.command?.rawQuotes; |
| cmd.unnamedArgumentList = this.parseUnnamedArgument(cmd.command?.unnamedArgumentList?.length && cmd?.command?.splitUnnamedArgument, cmd?.command?.splitUnnamedArgumentCount, rawQuotes); |
| 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); |
| } |
| } else if (cmd.name == 'import') { |
| const value = (cmd.unnamedArgumentList.map(it=>it.value)); |
| for (let i = 0; i < value.length; i++) { |
| const srcName = value[i]; |
| let dstName = srcName; |
| if (i + 2 < value.length && value[i + 1] == 'as') { |
| dstName = value[i + 2]; |
| i += 2; |
| } |
| this.scope.variableNames.push(dstName); |
| } |
| } |
| } |
| 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(); |
| this.take(); |
| 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, splitCount = null, rawQuotes = false) { |
| const wasSplit = split; |
| |
| let value = this.jumpedEscapeSequence ? this.take() : ''; |
| let isList = split; |
| let listValues = []; |
| let listQuoted = []; |
| |
| let assignment = new SlashCommandUnnamedArgumentAssignment(); |
| assignment.start = this.index; |
| if (!split && !rawQuotes && this.testQuotedValue()) { |
| |
| assignment.value = this.parseQuotedValue(); |
| assignment.end = this.index; |
| isList = true; |
| listValues.push(assignment); |
| listQuoted.push(true); |
| assignment = new SlashCommandUnnamedArgumentAssignment(); |
| assignment.start = this.index; |
| } |
| while (!this.testUnnamedArgumentEnd()) { |
| if (split && splitCount && listValues.length >= splitCount) { |
| |
| split = false; |
| if (this.testQuotedValue()) { |
| |
| assignment.value = this.parseQuotedValue(); |
| assignment.end = this.index; |
| listValues.push(assignment); |
| listQuoted.push(true); |
| assignment = new SlashCommandUnnamedArgumentAssignment(); |
| assignment.start = this.index; |
| } |
| } |
| if (this.testClosure()) { |
| isList = true; |
| if (value.length > 0) { |
| this.indexMacros(this.index - value.length, value); |
| assignment.value = value; |
| listValues.push(assignment); |
| listQuoted.push(false); |
| assignment = new SlashCommandUnnamedArgumentAssignment(); |
| assignment.start = this.index; |
| if (!split && this.testQuotedValue()) { |
| |
| assignment.value = this.parseQuotedValue(); |
| assignment.end = this.index; |
| listValues.push(assignment); |
| listQuoted.push(true); |
| assignment = new SlashCommandUnnamedArgumentAssignment(); |
| assignment.start = this.index; |
| } else { |
| value = ''; |
| } |
| } |
| assignment.start = this.index; |
| assignment.value = this.parseClosure(); |
| assignment.end = this.index; |
| listValues.push(assignment); |
| assignment = new SlashCommandUnnamedArgumentAssignment(); |
| assignment.start = this.index; |
| if (split) this.discardWhitespace(); |
| } else if (split) { |
| if (this.testQuotedValue()) { |
| assignment.start = this.index; |
| assignment.value = this.parseQuotedValue(); |
| assignment.end = this.index; |
| listValues.push(assignment); |
| listQuoted.push(true); |
| assignment = new SlashCommandUnnamedArgumentAssignment(); |
| } else if (this.testListValue()) { |
| assignment.start = this.index; |
| assignment.value = this.parseListValue(); |
| assignment.end = this.index; |
| listValues.push(assignment); |
| listQuoted.push(false); |
| assignment = new SlashCommandUnnamedArgumentAssignment(); |
| } else if (this.testValue()) { |
| assignment.start = this.index; |
| assignment.value = this.parseValue(); |
| assignment.end = this.index; |
| listValues.push(assignment); |
| listQuoted.push(false); |
| 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.length > 0) { |
| assignment.value = value; |
| listValues.push(assignment); |
| listQuoted.push(false); |
| } |
| if (isList) { |
| const firstVal = listValues[0]; |
| if (typeof firstVal?.value == 'string') { |
| if (!listQuoted[0]) { |
| |
| firstVal.value = firstVal.value.trimStart(); |
| } |
| if (firstVal.value.length == 0) { |
| listValues.shift(); |
| listQuoted.shift(); |
| } |
| } |
| const lastVal = listValues.slice(-1)[0]; |
| if (typeof lastVal?.value == 'string') { |
| if (!listQuoted.slice(-1)[0]) { |
| |
| lastVal.value = lastVal.value.trimEnd(); |
| } |
| if (lastVal.value.length == 0) { |
| listValues.pop(); |
| listQuoted.pop(); |
| } |
| } |
| if (wasSplit && splitCount && splitCount + 1 < listValues.length) { |
| |
| |
| |
| const joined = new SlashCommandUnnamedArgumentAssignment(); |
| joined.start = listValues[splitCount].start; |
| joined.end = listValues.slice(-1)[0].end; |
| joined.value = ''; |
| for (let i = splitCount; i < listValues.length; i++) { |
| if (listQuoted[i]) joined.value += `"${listValues[i].value}"`; |
| else joined.value += listValues[i].value; |
| } |
| listValues = [ |
| ...listValues.slice(0, splitCount), |
| joined, |
| ]; |
| } |
| 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(); |
| let value = ''; |
| while (!this.testQuotedValueEnd()) value += this.take(); |
| this.take(); |
| 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(); |
| while (!this.testListValueEnd()) value += this.take(); |
| value += this.take(); |
| 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() : ''; |
| while (!this.testValueEnd()) value += this.take(); |
| 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)); |
| } |
| } |
| } |
|
|