|
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 { MacroAutoCompleteOption } from '../autocomplete/MacroAutoCompleteOption.js'; |
|
import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js'; |
|
import { SlashCommandDebugController } from './SlashCommandDebugController.js'; |
|
import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js'; |
|
import { SlashCommandBreak } from './SlashCommandBreak.js'; |
|
import { MacrosParser } from '../macros.js'; |
|
import { t } from '../i18n.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 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), |
|
)); |
|
for (const macro of MacrosParser) { |
|
if (options.find(it => it.name === macro.key)) continue; |
|
options.push(new MacroAutoCompleteOption(macro.key, `{{${macro.key}}}`, macro.description || t`No description provided`)); |
|
} |
|
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 { } |
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(); |
|
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)); |
|
} |
|
} |
|
} |
|
|