2024-05-12 21:15:05 +02:00
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' ;
2024-06-20 20:33:45 +02:00
import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js' ;
2024-05-12 21:15:05 +02:00
2024-06-16 02:59:37 +02:00
/** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */
/** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */
2024-05-12 21:15:05 +02:00
/**@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
2024-06-16 02:59:37 +02:00
* @ param { ( namedArguments : NamedArguments | NamedArgumentsCapture , unnamedArguments : string | SlashCommandClosure | ( string | SlashCommandClosure ) [ ] ) => string | SlashCommandClosure | Promise < string | SlashCommandClosure > } callback callback The function to execute when the command is called
2024-05-12 21:15:05 +02:00
* @ 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 ( ) {
2024-05-19 21:52:30 +02:00
return this . index >= this . text . length || ( /\s/ . test ( this . char ) && /^\s+$/ . test ( this . ahead ) ) ;
2024-05-12 21:15:05 +02:00
}
constructor ( ) {
// 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' ,
2024-06-20 20:33:45 +02:00
enumList : commonEnumProviders . boolean ( 'onOff' ) ( ) ,
2024-05-12 21:15:05 +02:00
} ) ,
] ,
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 ,
} ;
2024-05-14 22:28:52 +02:00
function getQuotedRunRegex ( ) {
try {
return new RegExp ( '(".+?(?<!\\\\)")|(\\S+?)' ) ;
} catch {
// fallback for browsers that don't support lookbehind
return /(".+?")|(\S+?)/ ;
}
}
2024-05-12 21:15:05 +02:00
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 : [
/\/:/ ,
2024-05-14 22:28:52 +02:00
getQuotedRunRegex ( ) ,
2024-05-12 21:15:05 +02:00
] ,
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 ) ;
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 ' ) . l e n g t h ;
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 {:
2024-06-14 23:48:41 +02:00
const textStart = this . index ;
2024-05-12 21:15:05 +02:00
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
}
2024-06-14 23:48:41 +02:00
closure . rawText = this . text . slice ( textStart , this . index ) ;
2024-05-12 21:15:05 +02:00
if ( ! isRoot ) this . take ( 2 ) ; // discard closing :}
if ( this . testSymbol ( '()' ) ) {
this . take ( 2 ) ; // discard ()
closure . executeNow = true ;
}
closureIndexEntry . end = this . index - 1 ;
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 ) {
this . indexMacros ( this . index - value . length , value ) ;
2024-06-14 23:48:41 +02:00
assignment . value = value ;
2024-05-12 21:15:05 +02:00
listValues . push ( assignment ) ;
assignment = new SlashCommandUnnamedArgumentAssignment ( ) ;
assignment . start = this . index ;
value = '' ;
}
2024-05-18 20:48:50 +02:00
assignment . start = this . index ;
2024-05-12 21:15:05 +02:00
assignment . value = this . parseClosure ( ) ;
assignment . end = this . index ;
listValues . push ( assignment ) ;
assignment = new SlashCommandUnnamedArgumentAssignment ( ) ;
assignment . start = this . index ;
2024-06-14 23:48:41 +02:00
if ( split ) this . discardWhitespace ( ) ;
2024-05-12 21:15:05 +02:00
} 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 ;
}
}
2024-06-14 23:48:41 +02:00
if ( isList && value . length > 0 ) {
assignment . value = value ;
2024-05-12 21:15:05 +02:00
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 ) ) ;
}
}
}