A generic parser for slash command text input
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 175 lines 4.4 kB view raw
1export type InteractionOption = string | number | boolean 2 3export interface Interaction { 4 command: string 5 text: string 6 subCommands?: string[] 7 options: { [name: string]: InteractionOption } 8} 9 10export interface OptionChoice { 11 name: string 12 value: string | number 13} 14 15export interface OptionDefinition { 16 name: string 17 type: OptionType 18 description?: string 19 options?: OptionDefinition[] 20 required?: boolean 21 min_value?: number 22 max_value?: number 23 min_length?: number 24 max_length?: number 25 choices?: OptionChoice[] 26} 27 28export enum OptionType { 29 sub_command = 1, 30 string = 2, 31 integer = 3, 32 boolean = 4, 33 number = 5, 34 attachment = 6, 35} 36 37const prefix = '/' 38 39export function isCommand(content: string): boolean { 40 try { 41 parseCommand(content) 42 return true 43 } catch { 44 return false 45 } 46} 47 48export function parseCommand( 49 content: string, 50): { command: string; text: string } { 51 if (!content) throw new Error('no content') 52 const matchedPrefix = content.toLowerCase().startsWith(prefix.toLowerCase()) 53 if (!matchedPrefix) throw new Error('no prefix (not a command)') 54 55 let text = content.slice(prefix.length).trim() 56 if (!text) throw new Error('no body after prefix') 57 58 const command = text.match(/^[^\s]+/i)?.[0] 59 if (!command) throw new Error('invalid command') 60 61 text = text.slice(command.length).trim() 62 63 return { command, text } 64} 65 66export function parseSubCommands( 67 content: string, 68 max = 2, 69): { subCommands: string[]; remaining: string } { 70 const args = content.split(/ +/).filter((arg) => arg.length > 0) 71 72 const subCommands: string[] = [] 73 74 for (let i = 0; i < max; i++) { 75 if (!args.length) break 76 if (args[0]?.endsWith(':')) break 77 if (subCommands.length > i) break 78 subCommands.push(args[0]) 79 args.shift() 80 } 81 82 return { subCommands, remaining: args.join(' ') } 83} 84 85export function parseOptions( 86 content: string, 87 template?: OptionDefinition | OptionDefinition[], 88): { [name: string]: InteractionOption } { 89 const args = content.split(/ +/).filter((arg) => arg.length > 0) 90 const options: { [name: string]: InteractionOption } = {} 91 92 let name = '' 93 let value = '' 94 95 args.forEach((arg: string) => { 96 if (arg.endsWith(':')) { 97 if (name) { 98 options[name] = value 99 value = '' 100 } 101 name = arg.slice(0, -1) 102 } else if (name) { 103 if (value.length) value += ' ' 104 value += arg 105 } else { 106 throw new Error('Invalid arguments') 107 } 108 }) 109 110 if (name) options[name] = value 111 112 if (template) { 113 flattenOptionDefinitions(Array.isArray(template) ? template : [template]) 114 .forEach((definition: OptionDefinition) => { 115 const { name, type, required, choices } = definition 116 const value = options[name] 117 if (value == null) { 118 if (required) throw new Error(`missing required option: ${name}`) 119 else return 120 } 121 const parsedValue = parseOptionValue(value, type) 122 if (parsedValue === undefined) { 123 throw new Error(`Invalid option ${name}: ${options[name]}`) 124 } 125 if (choices && !choices.find(({ value }) => value === parsedValue)) { 126 throw new Error( 127 `Option value ${name}: ${parsedValue} is not one of the choices ${choices}`, 128 ) 129 } 130 options[name] = parsedValue 131 }) 132 } 133 134 return options 135} 136 137function flattenOptionDefinitions( 138 definitions: OptionDefinition[], 139): OptionDefinition[] { 140 let all_options: OptionDefinition[] = [] 141 definitions.forEach(({ options, ...def }) => { 142 all_options.push(def) 143 if (options) { 144 all_options = all_options.concat(flattenOptionDefinitions(options)) 145 } 146 }) 147 return all_options 148} 149 150export function parseOptionValue( 151 value: InteractionOption, 152 type: OptionType, 153): InteractionOption | void { 154 switch (type) { 155 case OptionType.attachment: 156 case OptionType.string: 157 return value 158 case OptionType.integer: 159 case OptionType.number: 160 return Number(value) 161 case OptionType.boolean: 162 return value === 'true' 163 } 164} 165 166export default function parse( 167 content: string, 168 template?: OptionDefinition | OptionDefinition[], 169): Interaction { 170 const { command, text } = parseCommand(content) 171 const { subCommands, remaining } = parseSubCommands(text) 172 const options = parseOptions(remaining, template) 173 174 return { command, text, options, subCommands } 175}