mirror of
https://github.com/ChipmunkMC/node-brigadier-commands.git
synced 2024-11-14 19:14:55 -05:00
286 lines
10 KiB
JavaScript
286 lines
10 KiB
JavaScript
const { CommandContextBuilder } = require('./context')
|
|
const { CommandSyntaxException } = require('./exceptions')
|
|
const { RootCommandNode } = require('./tree')
|
|
const { Suggestions, SuggestionsBuilder } = require('./suggestion')
|
|
const ParseResults = require('./ParseResults.js')
|
|
const StringReader = require('./StringReader.js')
|
|
|
|
const USAGE_OPTIONAL_OPEN = '['
|
|
const USAGE_OPTIONAL_CLOSE = ']'
|
|
const USAGE_REQUIRED_OPEN = '('
|
|
const USAGE_REQUIRED_CLOSE = ')'
|
|
const USAGE_OR = '|'
|
|
|
|
class CommandDispatcher {
|
|
static ARGUMENT_SEPARATOR = ' '
|
|
static ARGUMENT_SEPARATOR_CHAR = ' '
|
|
|
|
consumer = (context, success, result) => {}
|
|
|
|
constructor (root = new RootCommandNode()) {
|
|
this.root = root
|
|
}
|
|
|
|
register (command) {
|
|
const node = command.build()
|
|
this.root.addChild(node)
|
|
return node
|
|
}
|
|
|
|
execute (input, source) {
|
|
let parse
|
|
|
|
if (input instanceof ParseResults) parse = input
|
|
else if (input instanceof StringReader) parse = this.parse(input, source)
|
|
else parse = this.parse(new StringReader(input), source)
|
|
|
|
if (parse.reader.canRead()) {
|
|
if (parse.exceptions.size === 1) throw parse.exceptions.values().next().value
|
|
if (parse.context.range.isEmpty()) throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand.createWithContext(parse.reader)
|
|
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument.createWithContext(parse.reader)
|
|
}
|
|
|
|
let result = 0
|
|
let successfulForks = 0
|
|
let forked = false
|
|
let foundCommand = false
|
|
const command = parse.reader.string
|
|
const original = parse.context.build(command)
|
|
let contexts = [original]
|
|
let next = null
|
|
|
|
// TODO: Maybe use guard clauses here?
|
|
while (contexts != null) {
|
|
for (let i = 0; i < contexts.length; i++) {
|
|
const context = contexts[i]
|
|
const child = context.child
|
|
if (child != null) {
|
|
forked ||= context.forks
|
|
if (child.hasNodes()) {
|
|
const modifier = context.modifier
|
|
if (modifier == null) {
|
|
if (next == null) next = []
|
|
next.push(child.copyFor(context.source))
|
|
} else {
|
|
try {
|
|
const results = modifier(context)
|
|
if (results.length !== 0) {
|
|
if (next == null) {
|
|
next = []
|
|
}
|
|
for (const source of results) {
|
|
next.add(child.copyFor(source))
|
|
}
|
|
} else {
|
|
foundCommand = true
|
|
}
|
|
} catch (error) {
|
|
if (!(error instanceof CommandSyntaxException)) throw error
|
|
this.consumer(context, false, 0)
|
|
if (!forked) throw error
|
|
}
|
|
}
|
|
}
|
|
} else if (context.command != null) {
|
|
foundCommand = true
|
|
try {
|
|
const value = context.command(context)
|
|
result += value
|
|
this.consumer(context, true, value)
|
|
successfulForks++
|
|
} catch (error) {
|
|
if (!(error instanceof CommandSyntaxException)) throw error
|
|
this.consumer(context, false, 0)
|
|
if (!forked) throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
contexts = next
|
|
next = null
|
|
}
|
|
|
|
if (!foundCommand) {
|
|
this.consumer(original, false, 0)
|
|
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand.createWithContext(parse.reader)
|
|
}
|
|
|
|
return forked ? successfulForks : result
|
|
}
|
|
|
|
parse (command, source) {
|
|
if (!(command instanceof StringReader)) command = new StringReader(command)
|
|
|
|
const context = new CommandContextBuilder(this, source, this.root, command.cursor)
|
|
return this.#parseNodes(this.root, command, context)
|
|
}
|
|
|
|
#parseNodes (node, originalReader, contextSoFar) {
|
|
const source = contextSoFar.source
|
|
let errors = null
|
|
let potentials = null
|
|
const cursor = originalReader.cursor
|
|
|
|
for (const child of node.getRelaventNodes(originalReader)) {
|
|
if (!child.canUse(source)) continue
|
|
const context = contextSoFar.copy()
|
|
const reader = new StringReader(originalReader)
|
|
try {
|
|
try {
|
|
child.parse(reader, context)
|
|
} catch (error) {
|
|
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherParseException.createWithContext(reader, error.message)
|
|
}
|
|
if (reader.canRead() && reader.peek() !== CommandDispatcher.ARGUMENT_SEPARATOR_CHAR) {
|
|
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherExpectedArgumentSeparator.createWithContext(reader)
|
|
}
|
|
} catch (error) {
|
|
if (errors == null) errors = new Map()
|
|
errors.set(child, error)
|
|
reader.cursor = cursor
|
|
continue
|
|
}
|
|
|
|
context.withCommand(child.command)
|
|
if (reader.canRead(child.redirect == null ? 2 : 1)) {
|
|
reader.skip()
|
|
if (child.redirect != null) {
|
|
const childContext = new CommandContextBuilder(this, source, child.redirect, reader.cursor)
|
|
const parse = this.#parseNodes(child.redirect, reader, childContext)
|
|
context.withChild(parse.context)
|
|
return new ParseResults(context, parse.reader, parse.exceptions)
|
|
} else {
|
|
const parse = this.#parseNodes(child, reader, context)
|
|
if (potentials == null) potentials = []
|
|
potentials.push(parse)
|
|
}
|
|
} else {
|
|
if (potentials == null) potentials = []
|
|
potentials.push(new ParseResults(context, reader, new Map()))
|
|
}
|
|
}
|
|
|
|
if (potentials != null) {
|
|
if (potentials.length > 1) {
|
|
potentials.sort((a, b) => {
|
|
if (!a.reader.canRead() && b.reader.canRead()) return -1
|
|
if (a.reader.canRead() && !b.reader.canRead()) return 1
|
|
if (a.exceptions.size === 0 && !b.exceptions.size === 0) return -1
|
|
if (!a.exceptions.size === 0 && b.exceptions.size === 0) return 1
|
|
return 0
|
|
})
|
|
}
|
|
return potentials[0]
|
|
}
|
|
|
|
return new ParseResults(contextSoFar, originalReader, errors == null ? new Map() : errors)
|
|
}
|
|
|
|
getAllUsage (node, source, restricted) {
|
|
const result = []
|
|
this.#getAllUsage(node, source, result, '', restricted)
|
|
return result
|
|
}
|
|
|
|
#getAllUsage (node, source, result, prefix, restricted) {
|
|
if (restricted && !node.canUse(source)) return
|
|
|
|
if (node.command != null) result.push(prefix)
|
|
|
|
if (node.redirect != null) {
|
|
const redirect = node.redirect === this.root ? '...' : '-> ' + node.redirect.getUsageText()
|
|
result.push(prefix.length === 0 ? node.getUsageText() + CommandDispatcher.ARGUMENT_SEPARATOR + redirect : prefix + CommandDispatcher.ARGUMENT_SEPARATOR + redirect)
|
|
} else if (node.getChildren().length !== 0) {
|
|
for (const child of node.getChildren()) {
|
|
this.#getAllUsage(child, source, result, prefix.length === 0 ? child.getUsageText() : prefix + CommandDispatcher.ARGUMENT_SEPARATOR + child.getUsageText(), restricted)
|
|
}
|
|
}
|
|
}
|
|
|
|
getSmartUsage (node, source) {
|
|
const result = new Map()
|
|
|
|
const optional = node.command != null
|
|
for (const child of node.getChildren()) {
|
|
const usage = this.#getSmartUsage(child, source, optional, false)
|
|
if (usage != null) result.set(child, usage)
|
|
}
|
|
return result
|
|
}
|
|
|
|
#getSmartUsage (node, source, optional, deep) {
|
|
if (!node.canUse(source)) return
|
|
|
|
const self = optional ? USAGE_OPTIONAL_OPEN + node.getUsageText() + USAGE_OPTIONAL_CLOSE : node.getUsageText()
|
|
const childOptional = node.command != null
|
|
const open = childOptional ? USAGE_OPTIONAL_OPEN : USAGE_REQUIRED_OPEN
|
|
const close = childOptional ? USAGE_OPTIONAL_CLOSE : USAGE_REQUIRED_CLOSE
|
|
|
|
if (!deep) {
|
|
if (node.redirect != null) {
|
|
const redirect = node.redirect === this.root ? '...' : '-> ' + node.redirect.getUsageText()
|
|
return self + CommandDispatcher.ARGUMENT_SEPARATOR + redirect
|
|
} else {
|
|
const children = node.getChildren().filter(c => c.canUse(source))
|
|
if (children.length === 1) {
|
|
const usage = this.#getSmartUsage(children[0], source, childOptional, childOptional)
|
|
if (usage != null) return self + CommandDispatcher.ARGUMENT_SEPARATOR + usage
|
|
} else if (children.length > 1) {
|
|
const childUsage = new Set()
|
|
for (const child of children) {
|
|
const usage = this.#getSmartUsage(child, source, childOptional, true)
|
|
if (usage != null) childUsage.add(usage)
|
|
}
|
|
if (childUsage.length === 1) {
|
|
const usage = childUsage.values().next().value
|
|
return self + CommandDispatcher.ARGUMENT_SEPARATOR + (childOptional ? USAGE_OPTIONAL_OPEN + usage + USAGE_OPTIONAL_CLOSE : usage)
|
|
} else if (childUsage.length > 1) {
|
|
let string = open
|
|
let count = 0
|
|
for (const child of children) {
|
|
if (count > 0) string += USAGE_OR
|
|
string += child.getUsageText()
|
|
count++
|
|
}
|
|
if (count > 0) {
|
|
string += close
|
|
return self + CommandDispatcher.ARGUMENT_SEPARATOR + string
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return self
|
|
}
|
|
|
|
async getCompletionSuggestions (parse, cursor = parse.reader.getTotalLength()) {
|
|
const context = parse.context
|
|
|
|
const nodeBeforeCursor = context.findSuggestionContext(cursor)
|
|
const parent = nodeBeforeCursor.parent
|
|
const start = Math.min(nodeBeforeCursor.startPos, cursor)
|
|
|
|
const fullInput = parse.reader.string
|
|
const truncatedInput = fullInput.substring(0, cursor)
|
|
const truncatedInputLowerCase = truncatedInput.toLowerCase()
|
|
const promises = []
|
|
for (const node of parent.getChildren()) {
|
|
let promise = Suggestions.empty()
|
|
try {
|
|
promise = node.listSuggestions(context.build(truncatedInput), new SuggestionsBuilder(truncatedInput, truncatedInputLowerCase, start))
|
|
} catch {
|
|
}
|
|
promises.push(promise)
|
|
}
|
|
|
|
try {
|
|
const suggestions = await Promise.all(promises)
|
|
return Suggestions.merge(fullInput, suggestions)
|
|
} catch {
|
|
// Errors are currently ignored to have the same behavior of brigadier (maybe this should be changed?)
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = CommandDispatcher
|