2024-05-12 21:15:05 +02:00
import { substituteParams } from '../../script.js' ;
2024-07-09 00:07:37 +02:00
import { delay , escapeRegex , uuidv4 } from '../utils.js' ;
2024-06-14 23:48:41 +02:00
import { SlashCommand } from './SlashCommand.js' ;
2024-05-12 21:15:05 +02:00
import { SlashCommandAbortController } from './SlashCommandAbortController.js' ;
2024-06-24 14:36:39 +02:00
import { SlashCommandBreak } from './SlashCommandBreak.js' ;
import { SlashCommandBreakController } from './SlashCommandBreakController.js' ;
2024-06-18 20:29:29 +02:00
import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js' ;
2024-05-12 21:15:05 +02:00
import { SlashCommandClosureResult } from './SlashCommandClosureResult.js' ;
2024-06-18 20:29:29 +02:00
import { SlashCommandDebugController } from './SlashCommandDebugController.js' ;
2024-07-06 00:05:22 +02:00
import { SlashCommandExecutionError } from './SlashCommandExecutionError.js' ;
2024-05-12 21:15:05 +02:00
import { SlashCommandExecutor } from './SlashCommandExecutor.js' ;
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js' ;
import { SlashCommandScope } from './SlashCommandScope.js' ;
export class SlashCommandClosure {
/**@type {SlashCommandScope}*/ scope ;
/**@type {boolean}*/ executeNow = false ;
// @ts-ignore
/**@type {SlashCommandNamedArgumentAssignment[]}*/ argumentList = [ ] ;
// @ts-ignore
/**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = [ ] ;
/**@type {SlashCommandExecutor[]}*/ executorList = [ ] ;
/**@type {SlashCommandAbortController}*/ abortController ;
2024-06-24 14:36:39 +02:00
/**@type {SlashCommandBreakController}*/ breakController ;
2024-06-18 20:29:29 +02:00
/**@type {SlashCommandDebugController}*/ debugController ;
2024-05-12 21:15:05 +02:00
/**@type {(done:number, total:number)=>void}*/ onProgress ;
2024-06-14 23:48:41 +02:00
/**@type {string}*/ rawText ;
2024-06-27 17:49:12 +02:00
/**@type {string}*/ fullText ;
/**@type {string}*/ parserContext ;
2024-07-09 00:07:37 +02:00
/**@type {string}*/ # source = uuidv4 ( ) ;
get source ( ) { return this . # source ; }
set source ( value ) {
this . # source = value ;
for ( const executor of this . executorList ) {
executor . source = value ;
}
}
2024-05-12 21:15:05 +02:00
/**@type {number}*/
get commandCount ( ) {
return this . executorList . map ( executor => executor . commandCount ) . reduce ( ( sum , cur ) => sum + cur , 0 ) ;
}
constructor ( parent ) {
this . scope = new SlashCommandScope ( parent ) ;
}
toString ( ) {
2024-06-23 17:30:54 +02:00
return ` [Closure] ${ this . executeNow ? '()' : '' } ` ;
2024-05-12 21:15:05 +02:00
}
/ * *
*
* @ param { string } text
* @ param { SlashCommandScope } scope
* @ returns
* /
substituteParams ( text , scope = null ) {
let isList = false ;
let listValues = [ ] ;
scope = scope ? ? this . scope ;
2024-07-28 14:33:33 +02:00
const escapeMacro = ( it , isAnchored = false ) => {
const regexText = escapeRegex ( it . key . replace ( /\*/g , '~~~WILDCARD~~~' ) )
. replaceAll ( '~~~WILDCARD~~~' , '(?:(?:(?!(?:::|}})).)*)' )
;
if ( isAnchored ) {
return ` ^ ${ regexText } $ ` ;
}
return regexText ;
} ;
2024-07-06 01:13:37 +02:00
const macroList = scope . macroList . toSorted ( ( a , b ) => {
if ( a . key . includes ( '*' ) && ! b . key . includes ( '*' ) ) return 1 ;
if ( ! a . key . includes ( '*' ) && b . key . includes ( '*' ) ) return - 1 ;
2024-07-27 21:31:23 +02:00
if ( a . key . includes ( '*' ) && b . key . includes ( '*' ) ) return b . key . indexOf ( '*' ) - a . key . indexOf ( '*' ) ;
2024-07-06 01:13:37 +02:00
return 0 ;
} ) ;
const macros = macroList . map ( it => escapeMacro ( it ) ) . join ( '|' ) ;
const re = new RegExp ( ` (?<pipe>{{pipe}})|(?:{{var::(?<var>[^ \\ s]+?)(?:::(?<varIndex>(?!}}).+))?}})|(?:{{(?<macro> ${ macros } )}}) ` ) ;
2024-05-12 21:15:05 +02:00
let done = '' ;
let remaining = text ;
while ( re . test ( remaining ) ) {
const match = re . exec ( remaining ) ;
const before = substituteParams ( remaining . slice ( 0 , match . index ) ) ;
const after = remaining . slice ( match . index + match [ 0 ] . length ) ;
2024-07-28 14:33:33 +02:00
const replacer = match . groups . pipe ? scope . pipe : match . groups . var ? scope . getVariable ( match . groups . var , match . groups . index ) : macroList . find ( it => it . key == match . groups . macro || new RegExp ( escapeMacro ( it , true ) ) . test ( match . groups . macro ) ) ? . value ;
2024-05-12 21:15:05 +02:00
if ( replacer instanceof SlashCommandClosure ) {
2024-06-27 17:49:12 +02:00
replacer . abortController = this . abortController ;
replacer . breakController = this . breakController ;
replacer . scope . parent = this . scope ;
if ( this . debugController && ! replacer . debugController ) {
replacer . debugController = this . debugController ;
}
2024-05-12 21:15:05 +02:00
isList = true ;
if ( match . index > 0 ) {
listValues . push ( before ) ;
}
listValues . push ( replacer ) ;
if ( match . index + match [ 0 ] . length + 1 < remaining . length ) {
const rest = this . substituteParams ( after , scope ) ;
listValues . push ( ... ( Array . isArray ( rest ) ? rest : [ rest ] ) ) ;
}
break ;
} else {
done = ` ${ done } ${ before } ${ replacer } ` ;
remaining = after ;
}
}
if ( ! isList ) {
text = ` ${ done } ${ substituteParams ( remaining ) } ` ;
}
if ( isList ) {
if ( listValues . length > 1 ) return listValues ;
return listValues [ 0 ] ;
}
return text ;
}
getCopy ( ) {
const closure = new SlashCommandClosure ( ) ;
closure . scope = this . scope . getCopy ( ) ;
closure . executeNow = this . executeNow ;
closure . argumentList = this . argumentList ;
closure . providedArgumentList = this . providedArgumentList ;
closure . executorList = this . executorList ;
closure . abortController = this . abortController ;
2024-06-24 14:36:39 +02:00
closure . breakController = this . breakController ;
2024-06-18 20:29:29 +02:00
closure . debugController = this . debugController ;
2024-06-27 17:49:12 +02:00
closure . rawText = this . rawText ;
closure . fullText = this . fullText ;
closure . parserContext = this . parserContext ;
2024-07-09 00:07:37 +02:00
closure . source = this . source ;
2024-05-12 21:15:05 +02:00
closure . onProgress = this . onProgress ;
return closure ;
}
/ * *
*
2024-06-20 19:06:58 +02:00
* @ returns { Promise < SlashCommandClosureResult > }
2024-05-12 21:15:05 +02:00
* /
async execute ( ) {
2024-07-24 23:50:57 +02:00
// execute a copy of the closure to no taint it and its scope with the effects of its execution
// as this would affect the closure being called a second time (e.g., loop, multiple /run calls)
2024-05-12 21:15:05 +02:00
const closure = this . getCopy ( ) ;
2024-06-18 20:29:29 +02:00
const gen = closure . executeDirect ( ) ;
let step ;
while ( ! step ? . done ) {
2024-06-20 21:53:30 +02:00
step = await gen . next ( this . debugController ? . testStepping ( this ) ? ? false ) ;
2024-06-18 20:29:29 +02:00
if ( ! ( step . value instanceof SlashCommandClosureResult ) && this . debugController ) {
this . debugController . isStepping = await this . debugController . awaitBreakPoint ( step . value . closure , step . value . executor ) ;
}
}
return step . value ;
}
async * executeDirect ( ) {
2024-06-20 19:06:58 +02:00
this . debugController ? . down ( this ) ;
2024-05-12 21:15:05 +02:00
// closure arguments
for ( const arg of this . argumentList ) {
let v = arg . value ;
if ( v instanceof SlashCommandClosure ) {
/**@type {SlashCommandClosure}*/
const closure = v ;
closure . scope . parent = this . scope ;
2024-06-24 14:36:39 +02:00
closure . breakController = this . breakController ;
2024-05-12 21:15:05 +02:00
if ( closure . executeNow ) {
v = ( await closure . execute ( ) ) ? . pipe ;
} else {
v = closure ;
}
} else {
v = this . substituteParams ( v ) ;
}
// unescape value
if ( typeof v == 'string' ) {
v = v
? . replace ( /\\\{/g , '{' )
? . replace ( /\\\}/g , '}' )
;
}
this . scope . letVariable ( arg . name , v ) ;
}
for ( const arg of this . providedArgumentList ) {
let v = arg . value ;
if ( v instanceof SlashCommandClosure ) {
/**@type {SlashCommandClosure}*/
const closure = v ;
closure . scope . parent = this . scope ;
2024-06-24 14:36:39 +02:00
closure . breakController = this . breakController ;
2024-05-12 21:15:05 +02:00
if ( closure . executeNow ) {
v = ( await closure . execute ( ) ) ? . pipe ;
} else {
v = closure ;
}
} else {
v = this . substituteParams ( v , this . scope . parent ) ;
}
// unescape value
if ( typeof v == 'string' ) {
v = v
? . replace ( /\\\{/g , '{' )
? . replace ( /\\\}/g , '}' )
;
}
this . scope . setVariable ( arg . name , v ) ;
}
2024-06-14 23:48:41 +02:00
if ( this . executorList . length == 0 ) {
this . scope . pipe = '' ;
}
2024-06-18 20:29:29 +02:00
const stepper = this . executeStep ( ) ;
let step ;
2024-06-24 14:36:39 +02:00
while ( ! step ? . done && ! this . breakController ? . isBreak ) {
2024-06-18 20:29:29 +02:00
// get executor before execution
step = await stepper . next ( ) ;
if ( step . value instanceof SlashCommandBreakPoint ) {
console . log ( 'encountered SlashCommandBreakPoint' ) ;
if ( this . debugController ) {
2024-07-24 23:50:57 +02:00
// resolve args
2024-06-18 20:29:29 +02:00
step = await stepper . next ( ) ;
2024-07-24 23:50:57 +02:00
// "execute" breakpoint
2024-06-23 17:31:07 +02:00
step = await stepper . next ( ) ;
2024-06-18 20:29:29 +02:00
// get next executor
step = await stepper . next ( ) ;
2024-07-24 23:50:57 +02:00
// breakpoint has to yield before arguments are resolved if one of the
// arguments is an immediate closure, otherwise you cannot step into the
// immediate closure
2024-07-19 00:06:17 +02:00
const hasImmediateClosureInNamedArgs = /**@type {SlashCommandExecutor}*/ ( step . value ) ? . namedArgumentList ? . find ( it => it . value instanceof SlashCommandClosure && it . value . executeNow ) ;
const hasImmediateClosureInUnnamedArgs = /**@type {SlashCommandExecutor}*/ ( step . value ) ? . unnamedArgumentList ? . find ( it => it . value instanceof SlashCommandClosure && it . value . executeNow ) ;
2024-06-23 17:31:07 +02:00
if ( hasImmediateClosureInNamedArgs || hasImmediateClosureInUnnamedArgs ) {
this . debugController . isStepping = yield { closure : this , executor : step . value } ;
} else {
this . debugController . isStepping = true ;
this . debugController . stepStack [ this . debugController . stepStack . length - 1 ] = true ;
}
2024-06-18 20:29:29 +02:00
}
2024-06-20 21:53:30 +02:00
} else if ( ! step . done && this . debugController ? . testStepping ( this ) ) {
2024-06-23 17:31:07 +02:00
this . debugController . isSteppingInto = false ;
2024-07-24 23:50:57 +02:00
// if stepping, have to yield before arguments are resolved if one of the arguments
// is an immediate closure, otherwise you cannot step into the immediate closure
2024-07-19 00:06:17 +02:00
const hasImmediateClosureInNamedArgs = /**@type {SlashCommandExecutor}*/ ( step . value ) ? . namedArgumentList ? . find ( it => it . value instanceof SlashCommandClosure && it . value . executeNow ) ;
const hasImmediateClosureInUnnamedArgs = /**@type {SlashCommandExecutor}*/ ( step . value ) ? . unnamedArgumentList ? . find ( it => it . value instanceof SlashCommandClosure && it . value . executeNow ) ;
2024-06-23 17:31:07 +02:00
if ( hasImmediateClosureInNamedArgs || hasImmediateClosureInUnnamedArgs ) {
this . debugController . isStepping = yield { closure : this , executor : step . value } ;
}
}
// resolve args
step = await stepper . next ( ) ;
2024-07-19 00:06:08 +02:00
if ( step . value instanceof SlashCommandBreak ) {
console . log ( 'encountered SlashCommandBreak' ) ;
if ( this . breakController ) {
this . breakController ? . break ( ) ;
break ;
}
} else if ( ! step . done && this . debugController ? . testStepping ( this ) ) {
2024-06-18 20:29:29 +02:00
this . debugController . isSteppingInto = false ;
this . debugController . isStepping = yield { closure : this , executor : step . value } ;
}
// execute executor
step = await stepper . next ( ) ;
}
// if execution has returned a closure result, return that (should only happen on abort)
if ( step . value instanceof SlashCommandClosureResult ) {
2024-06-20 19:06:58 +02:00
this . debugController ? . up ( ) ;
2024-06-18 20:29:29 +02:00
return step . value ;
}
2024-05-12 21:15:05 +02:00
/**@type {SlashCommandClosureResult} */
2024-06-24 14:36:39 +02:00
const result = Object . assign ( new SlashCommandClosureResult ( ) , { pipe : this . scope . pipe , isBreak : this . breakController ? . isBreak ? ? false } ) ;
2024-06-20 19:06:58 +02:00
this . debugController ? . up ( ) ;
2024-05-12 21:15:05 +02:00
return result ;
}
2024-07-24 23:50:57 +02:00
/ * *
* Generator that steps through the executor list .
* Every executor is split into three steps :
* - before arguments are resolved
* - after arguments are resolved
* - after execution
* /
2024-06-18 20:29:29 +02:00
async * executeStep ( ) {
let done = 0 ;
2024-07-04 18:35:56 +02:00
let isFirst = true ;
2024-06-18 20:29:29 +02:00
for ( const executor of this . executorList ) {
this . onProgress ? . ( done , this . commandCount ) ;
2024-07-25 03:00:58 +02:00
if ( this . debugController ) {
this . debugController . setExecutor ( executor ) ;
this . debugController . namedArguments = undefined ;
this . debugController . unnamedArguments = undefined ;
}
2024-07-24 23:50:57 +02:00
// yield before doing anything with this executor, the debugger might want to do
// something with it (e.g., breakpoint, immediate closures that need resolving
// or stepping into)
2024-06-18 20:29:29 +02:00
yield executor ;
2024-07-19 00:06:08 +02:00
/**@type {import('./SlashCommand.js').NamedArguments} */
// @ts-ignore
let args = {
_scope : this . scope ,
_parserFlags : executor . parserFlags ,
_abortController : this . abortController ,
_debugController : this . debugController ,
_hasUnnamedArgument : executor . unnamedArgumentList . length > 0 ,
} ;
2024-06-24 13:51:44 +02:00
if ( executor instanceof SlashCommandBreakPoint ) {
2024-07-24 23:50:57 +02:00
// nothing to do for breakpoints, just raise counter and yield for "before exec"
2024-06-18 20:29:29 +02:00
done ++ ;
2024-06-23 17:31:07 +02:00
yield executor ;
2024-07-04 18:35:56 +02:00
isFirst = false ;
2024-06-24 14:36:39 +02:00
} else if ( executor instanceof SlashCommandBreak ) {
2024-07-24 23:50:57 +02:00
// /break need to resolve the unnamed arg and put it into pipe, then yield
// for "before exec"
2024-07-19 00:06:08 +02:00
const value = await this . substituteUnnamedArgument ( executor , isFirst , args ) ;
2024-06-24 14:36:39 +02:00
done += this . executorList . length - this . executorList . indexOf ( executor ) ;
2024-07-19 00:06:08 +02:00
this . scope . pipe = value ? ? this . scope . pipe ;
2024-06-24 14:36:39 +02:00
yield executor ;
2024-07-04 18:35:56 +02:00
isFirst = false ;
2024-06-18 20:29:29 +02:00
} else {
2024-07-24 23:50:57 +02:00
// regular commands do all the argument resolving logic...
2024-07-19 00:06:08 +02:00
await this . substituteNamedArguments ( executor , args ) ;
let value = await this . substituteUnnamedArgument ( executor , isFirst , args ) ;
2024-06-18 20:29:29 +02:00
let abortResult = await this . testAbortController ( ) ;
if ( abortResult ) {
return abortResult ;
}
2024-06-23 17:31:07 +02:00
if ( this . debugController ) {
this . debugController . namedArguments = args ;
this . debugController . unnamedArguments = value ? ? '' ;
}
2024-07-24 23:50:57 +02:00
// then yield for "before exec"
2024-06-23 17:31:07 +02:00
yield executor ;
2024-07-24 23:50:57 +02:00
// followed by command execution
2024-06-18 20:29:29 +02:00
executor . onProgress = ( subDone , subTotal ) => this . onProgress ? . ( done + subDone , this . commandCount ) ;
2024-06-20 21:53:30 +02:00
const isStepping = this . debugController ? . testStepping ( this ) ;
2024-06-18 20:29:29 +02:00
if ( this . debugController ) {
this . debugController . isStepping = false || this . debugController . isSteppingInto ;
}
2024-07-06 00:05:22 +02:00
try {
this . scope . pipe = await executor . command . callback ( args , value ? ? '' ) ;
} catch ( ex ) {
throw new SlashCommandExecutionError ( ex , ex . message , executor . name , executor . start , executor . end , this . fullText . slice ( executor . start , executor . end ) , this . fullText ) ;
}
2024-06-18 20:29:29 +02:00
if ( this . debugController ) {
2024-06-23 17:31:07 +02:00
this . debugController . namedArguments = undefined ;
this . debugController . unnamedArguments = undefined ;
2024-06-18 20:29:29 +02:00
this . debugController . isStepping = isStepping ;
}
this . # lintPipe ( executor . command ) ;
done += executor . commandCount ;
this . onProgress ? . ( done , this . commandCount ) ;
abortResult = await this . testAbortController ( ) ;
if ( abortResult ) {
return abortResult ;
}
}
2024-07-24 23:50:57 +02:00
// finally, yield for "after exec"
2024-06-18 20:29:29 +02:00
yield executor ;
2024-07-04 18:35:56 +02:00
isFirst = false ;
2024-06-18 20:29:29 +02:00
}
}
2024-05-12 21:15:05 +02:00
async testPaused ( ) {
while ( ! this . abortController ? . signal ? . aborted && this . abortController ? . signal ? . paused ) {
await delay ( 200 ) ;
}
}
async testAbortController ( ) {
await this . testPaused ( ) ;
if ( this . abortController ? . signal ? . aborted ) {
const result = new SlashCommandClosureResult ( ) ;
result . isAborted = true ;
2024-05-18 20:48:31 +02:00
result . isQuietlyAborted = this . abortController . signal . isQuiet ;
2024-05-12 21:15:05 +02:00
result . abortReason = this . abortController . signal . reason . toString ( ) ;
return result ;
}
}
2024-06-14 23:48:41 +02:00
2024-07-19 00:06:08 +02:00
/ * *
* @ param { SlashCommandExecutor } executor
* @ param { import ( './SlashCommand.js' ) . NamedArguments } args
* /
async substituteNamedArguments ( executor , args ) {
// substitute named arguments
for ( const arg of executor . namedArgumentList ) {
if ( arg . value instanceof SlashCommandClosure ) {
/**@type {SlashCommandClosure}*/
const closure = arg . value ;
closure . scope . parent = this . scope ;
closure . breakController = this . breakController ;
if ( this . debugController && ! closure . debugController ) {
closure . debugController = this . debugController ;
}
if ( closure . executeNow ) {
args [ arg . name ] = ( await closure . execute ( ) ) ? . pipe ;
} else {
args [ arg . name ] = closure ;
}
} else {
args [ arg . name ] = this . substituteParams ( arg . value ) ;
}
// unescape named argument
if ( typeof args [ arg . name ] == 'string' ) {
args [ arg . name ] = args [ arg . name ]
? . replace ( /\\\{/g , '{' )
? . replace ( /\\\}/g , '}' )
;
}
}
}
/ * *
* @ param { SlashCommandExecutor } executor
* @ param { boolean } isFirst
* @ param { import ( './SlashCommand.js' ) . NamedArguments } args
* @ returns { Promise < string | SlashCommandClosure | ( string | SlashCommandClosure ) [ ] > }
* /
async substituteUnnamedArgument ( executor , isFirst , args ) {
let value ;
// substitute unnamed argument
if ( executor . unnamedArgumentList . length == 0 ) {
if ( ! isFirst && executor . injectPipe ) {
value = this . scope . pipe ;
args . _hasUnnamedArgument = this . scope . pipe !== null && this . scope . pipe !== undefined ;
}
} else {
value = [ ] ;
for ( let i = 0 ; i < executor . unnamedArgumentList . length ; i ++ ) {
let v = executor . unnamedArgumentList [ i ] . value ;
if ( v instanceof SlashCommandClosure ) {
/**@type {SlashCommandClosure}*/
const closure = v ;
closure . scope . parent = this . scope ;
closure . breakController = this . breakController ;
if ( this . debugController && ! closure . debugController ) {
closure . debugController = this . debugController ;
}
if ( closure . executeNow ) {
v = ( await closure . execute ( ) ) ? . pipe ;
} else {
v = closure ;
}
} else {
v = this . substituteParams ( v ) ;
}
value [ i ] = v ;
}
if ( ! executor . command . splitUnnamedArgument ) {
if ( value . length == 1 ) {
value = value [ 0 ] ;
} else if ( ! value . find ( it => it instanceof SlashCommandClosure ) ) {
value = value . join ( '' ) ;
}
}
}
// unescape unnamed argument
if ( typeof value == 'string' ) {
value = value
? . replace ( /\\\{/g , '{' )
? . replace ( /\\\}/g , '}' )
;
} else if ( Array . isArray ( value ) ) {
value = value . map ( v => {
if ( typeof v == 'string' ) {
return v
? . replace ( /\\\{/g , '{' )
? . replace ( /\\\}/g , '}' ) ;
}
return v ;
} ) ;
}
return value ;
}
2024-06-14 23:48:41 +02:00
/ * *
* Auto - fixes the pipe if it is not a valid result for STscript .
* @ param { SlashCommand } command Command being executed
* /
# lintPipe ( command ) {
if ( this . scope . pipe === undefined || this . scope . pipe === null ) {
2024-07-09 14:21:57 +02:00
console . warn ( ` / ${ command . name } returned undefined or null. Auto-fixing to empty string. ` ) ;
2024-06-14 23:48:41 +02:00
this . scope . pipe = '' ;
2024-07-09 14:21:57 +02:00
} else if ( ! ( typeof this . scope . pipe == 'string' || this . scope . pipe instanceof SlashCommandClosure ) ) {
console . warn ( ` / ${ command . name } returned illegal type ( ${ typeof this . scope . pipe } - ${ this . scope . pipe . constructor ? . name ? ? '' } ). Auto-fixing to stringified JSON. ` ) ;
this . scope . pipe = JSON . stringify ( this . scope . pipe ) ? ? '' ;
2024-06-14 23:48:41 +02:00
}
}
2024-05-12 21:15:05 +02:00
}