UNPKG

tcl-js

Version:

tcl-js is a tcl intepreter written completely in Typescript. It is meant to replicate the tcl-sh interpreter as closely as possible.

803 lines (726 loc) 21.2 kB
import * as Is from './is'; import { TclError } from './tclerror'; import { Interpreter } from './interpreter'; import { CommandToken } from './parser'; import { isArray } from 'util'; /** * The basic structure for any variable in this interpreter * * @export * @class TclVariable */ export class TclVariable { protected value: any = ''; protected name: string | undefined = undefined; /** * Construct a parent TclVariable, you should never be directly constructing this object. * * @param {*} value * @param {string} [name] * @memberof TclVariable */ public constructor(value: any, name?: string) { this.value = value; if (name) this.name = name; } /** * Get the value of any tcl type, this will always be a string * * @returns {string} * @memberof TclVariable */ public getValue(): string { return this.value; } // Not needed /** * Set the value of any tcl type, this can be anything * * @param {any} value * @returns {any} * @memberof TclVariable */ /* public setValue(value: any): any { this.value = value; return value; }*/ // Not needed /** * This function should not be used but is here in case you need it for testing * It return the raw interal data storage of the variable * * @returns {*} * @memberof TclVariable */ /* public getRawValue(): any { return this.value; }*/ /** * This function returns the name of the variable, as long as it has one * * @returns {(string | undefined)} * @memberof TclVariable */ public getName(): string | undefined { return this.name; } /** * This function sets the name of the variable * * @param {string} name * @memberof TclVariable */ public setName(name: string) { this.name = name; } } /** * A variable for holding a list, this is created from a raw string most of the time * * @export * @class TclList * @extends {TclVariable} */ export class TclList extends TclVariable { /** * Creates an instance of TclList. * * @param {(string | Array<TclVariable>)} - The string to parse into the variable * @param {string} [name] - An optional name for the variable * @memberof TclList */ public constructor(value: string | Array<TclVariable>, name?: string) { super([], name); if (typeof value === 'string') this.destruct(value); else this.value = value; } /** * Called to parse a string into the internal value array * * @private * @param {string} input - The list input you want to parse * @memberof TclList */ private destruct(input: string): void { // Initialize a counter for keeping track of the posisition in the string let idx = 0; // Initialize a char with the first character of the string let char = input.charAt(idx); /** * This function is used for progressing to the next char in the string * * @returns {string} - This is the previous char */ function read(): string { let old = char; idx += 1; char = input.charAt(idx); return old; } /** * This function is used to parse the braces in lists * * This is because words enclosed in bracets should be seen as one list item * E.g. 'hi {hello there}' => ['hi', 'hello there'] * and 'hi {this is a {nested list}} wow' => ['hi', 'this is a {nested list}', 'wow'] * * @returns {string} */ function parseBrace(): string { // Initialize a string to keep the return value let returnVar = ''; // Intialize a variable to keep track of how many braces deep we are let depth = 0; // Keep reading the string as long as we have input while (idx < input.length) { // Increase or decrease the depth depending on the brackets if (char === '{') { depth++; // Ignore the outer brace if (depth === 1) { read(); continue; } } if (char === '}') { depth--; // Ignore the outer brace if (depth === 0) { read(); break; } } // Add the next character to the output returnVar += read(); } // This is true when there are more closing brackets than opening brackets if (depth !== 0) throw new TclError('incorrect brackets in list'); // Check if the character following the } is a whitespace if (!Is.WordSeparator(char) && char !== '') throw new TclError( 'list element in braces followed by character instead of space', ); return returnVar; } // Initialize a counter for keeping track of the current list index let i = 0; // Keep reading until there is no more string to read while (idx < input.length) { // Initialize an empty string to contain the next list item let tempWord = ''; // Skip all whitespace while (Is.WordSeparator(char) && idx < input.length) { read(); } // Parse the braces if a brace is found if (char === '{') { tempWord += parseBrace(); } // Just add the characters to the output if not else { while (!Is.WordSeparator(char) && idx < input.length) { tempWord += read(); } } // Check if there was actually data left to read if (tempWord === '') break; // Set the value correctly this.value[i] = new TclSimple(tempWord); // Increment the item index i++; // Move to the next char and word, regardless if the index has incremented read(); } } // Not yet needed /** * Function to set an item in the list to a value * * @param {number} index - The index in the list you want to set to the value * @param {TclSimple} [value] - The value you want to add, if this empty the item will be removed * @returns {(TclSimple | undefined)} - The value you sent via the value argument * @memberof TclList */ /* public set(index: number, value?: TclSimple): TclSimple | undefined { // If the value is nonexitant we want to delete the item if (!value) { // We cannot delete an item that does not exist if (!this.value[index]) throw new TclError('cannot delete list item, item does not exist'); // Remove the item from the array this.value.splice(index, 1); } else { // If we dont want to delete we just set the value this.value[index] = value; } return value; }*/ // Not yet needed /** * Delete a value from the list * * @param {number} index * @memberof TclList */ /* public unset(index: number): void { // Use the set function to delete this.set(index); }*/ /** * Get the string value of the list, this is made by joining the values with a space * * @returns {string} - The joined array * @memberof TclList */ public getValue(): string { let toReturn = this.value.map((val: TclSimple) => val.getValue()); toReturn = toReturn.map((val: string) => val.indexOf(' ') > -1 ? `{${val}}` : val, ); return toReturn.join(' '); } /** * This function is used to recursively retrieve items from a list, every argument is one list deeper * * @param {...Array<number>} args - The indexes of the lists * @returns {TclSimple} - The eventually retrieved value * @memberof TclList */ public getSubValue(...args: Array<number>): TclSimple { // There are no arguments, so just return a TclSimple with the current value if (args.length === 0) return new TclSimple(this.getValue(), this.getName()); // There is only one argument, so if available return the value at that index if (args.length === 1) { if (this.value[args[0]]) return this.value[args[0]]; // If not return an empty TclSimple, according to the tcl wiki else return new TclSimple(''); } // Code reaches here when there are more than 1 arguments // Create a variable for keeping the current list let tempList: TclList = this; // Create a variable for keeping the eventual return value let out: TclSimple = new TclSimple(''); // Loop over the received arguments for (let arg of args) { // Throw an error when there is no list available // if (!tempList) throw new TclError('item is no list'); // Retrieve the value from the list at the current index=arg and assign this to the output out = tempList.getSubValue(arg); // we create a list out of it and assign that to the tempList tempList = out.getList(); /* TODO: Check if this can be commented // If the output is a TclSimple we create a list out of it and assign that to the tempList if (out instanceof TclSimple) tempList = out.getList(); // If not tempList will be undefined else tempList = undefined;*/ } // TODO: Check if this can be commented // Throw an error when there is still no return value // if (!out) throw new TclError('no such element in array'); return out; } /** * Get the length of the list * * @returns {number} * @memberof TclList */ public getLength(): number { return this.value.length; } /** * Function to create a list from an array of arrays * * @static * @param {Array<any>} input - The nested arrays * @returns {TclSimple} - The generated list * @memberof TclList */ public static createList(input: Array<any>): TclSimple { let processable = [...input]; for (let i = 0; i < processable.length; i++) { if (isArray(processable[i])) { processable[i] = TclList.createList(processable[i]); } } let simpleResults = processable.map((r) => r instanceof TclVariable ? r : new TclSimple(r), ); let listResult = new TclList(simpleResults).getSubValue(); return listResult; } } /** * A tcl variable for holding simple values like strings and numbers. * Although numbers will still be stored as a string instead of a number, and converted on request. * * @export * @class TclSimple * @extends {TclVariable} */ export class TclSimple extends TclVariable { /** *Creates an instance of TclSimple. * @param {(string | boolean | number)} value - The initial value * @param {string} [name] - Variable name * @memberof TclSimple */ constructor(value: string | boolean | number, name?: string) { super(value.toString(), name); } /** * This function will generate a new TclList object from the current value it holds * * @returns {TclList} - The created list * @memberof TclSimple */ public getList(): TclList { let list = new TclList(this.value, this.getName()); return list; } /** * Function to convert the TclSimple to a js number if the value allows this * * @param {boolean} [isInt=false] - Tell the function to return an int or a float * @returns {number} - The returned number, 0 if the variable is not a number * @memberof TclSimple */ public getNumber(isInt: boolean = false): number { if (this.isNumber()) return isInt ? parseInt(this.value, 10) : parseFloat(this.value); else if (this.isBoolean()) return this.getBoolean() ? 1 : 0; else return 0; } /** * Check if this TclSimple can be converted to a number * * @returns {boolean} * @memberof TclSimple */ public isNumber(): boolean { return Is.Number(this.value); } /** * Convert this TclSimple to a boolean * * @returns {boolean} * @memberof TclSimple */ public getBoolean(): boolean { if ( this.value === 'true' || this.value === 'on' || this.value === 'yes' || this.value === '1' ) return true; else if ( this.value === 'false' || this.value === 'off' || this.value === 'no' || this.value === '0' ) return false; else if (this.value) return true; else return false; } /** * Check if this TclSimple can be converted to a boolean * * @returns {boolean} * @memberof TclSimple */ public isBoolean(): boolean { return Is.Boolean(this.value); } } /** * This is a variable for holding objects in tcl, these could be compared to the objects in normal JS * * @export * @class TclObject * @extends {TclVariable} */ export class TclObject extends TclVariable { /** * Creates an instance of TclObject. * * @param {TclVariableHolder} [value] - Will set the internal value to empty if none is parsed * @param {string} [name] * @memberof TclObject */ constructor(value?: TclVariableHolder, name?: string) { super(value, name); if (!this.value) this.value = {}; } /** * Function to set a value at a object key * * @param {string} name - The key to put the value at * @param {TclVariable} [value] - If this is undefined, the object key will be deleted * @returns {(TclVariable | undefined)} - The value parsed * @memberof TclObject */ public set(name: string, value?: TclVariable): TclVariable | undefined { // If value is empty delete value from the internal object if (!value) { if(Object.keys(this.value).indexOf(name) < 0) throw new TclError('cannot delete object item, item does not exist'); delete this.value[name]; } // If there is data append it to the correct key else this.value[name] = value; return value; } /** * Remove a key from the object * * @param {string} name - The key you want to remove * @memberof TclObject */ public unset(name: string): void { // Use the set function to do the job this.set(name); } /** * You are not meant to directly get the value of an object, so throw an error * * @returns {string} * @throws {TclError} * @memberof TclObject */ public getValue(): string { throw new TclError(`can't read "${this.getName()}": variable is object`); } /** * Get a value from a specified key in the object * * @param {string} name - The key you want the value from * @returns {TclVariable} - The value * @memberof TclObject */ public getSubValue(name: string): TclVariable { // Return this value when no name is specified if (name === '') return new TclSimple(this.getValue(), this.getName()); // Throw error when key does not exist if (!this.value[name]) throw new TclError(`no value found at given key: ${name}`); return this.value[name]; } /** * Get all object keys that are present * * @returns {string[]} * @memberof TclObject */ public getKeys(): string[] { return Object.keys(this.value); } /** * Get the size of the object * * @returns {number} - Size * @memberof TclObject */ public getSize(): number { return Object.keys(this.value).length; } } /** * A variable for holding a tcl array, this is comparable to the standard JS array * * @export * @class TclArray * @extends {TclVariable} */ export class TclArray extends TclVariable { /** * Creates an instance of TclArray. * * @param {Array<TclVariable>} [value] - Will set the value to an empty array if nothing is parsed * @param {string} [name] * @memberof TclArray */ public constructor(value?: Array<TclVariable>, name?: string) { super(value, name); if (!this.value) this.value = []; } /** * Set a value to a specified index in the array and return the value * * @param {number} index - The index you want the value to be set at * @param {TclVariable} [value] - The value you want to set, leave empty to remove the index * @returns {(TclVariable | undefined)} - The value specified * @memberof TclArray */ public set(index: number, value?: TclVariable): TclVariable | undefined { // If the value is nonexitant we want to delete the item if (!value) { // We cannot delete an item that does not exist if (!this.value[index]) throw new TclError('cannot delete array item, item does not exist'); // Remove the item from the array delete this.value[index]; } else { // If we dont want to delete we just set the value this.value[index] = value; } return value; } /** * Remove an index from the array * * @param {number} index * @memberof TclArray */ public unset(index: number): void { // Use the set function to remove the index this.set(index); } /** * You are not meant to directly get the value of an array, so throw an error * * @returns {string} * @memberof TclArray */ public getValue(): string { throw new TclError(`can't read "${this.getName()}": variable is array`); } /** * Get the value at a specified index * * @param {number} index - The index you want the value from * @param {boolean} [force] - If true, wont throw error on nonexistant index (please avoid using this) * @returns {TclVariable} - The found value * @memberof TclArray */ public getSubValue(index: number, force?: boolean): TclVariable { // If index is not correct return the value of this variable if (index === undefined || index === null) return new TclSimple(this.getValue(), this.getName()); // Throw error if index does not exist if (!this.value[index]) { if (force) return new TclVariable(undefined); else throw new TclError(`no value found at given index: ${index}`); } return this.value[index]; } /** * Get the length of the array * * @returns {number} * @memberof TclArray */ public getLength(): number { return this.value.length; } } /** * A simple interface for an object that stores TclVariables with a string index * * @export * @interface TclVariableHolder */ export interface TclVariableHolder { [index: string]: TclVariable; } /** * A simple interface for an object that holds tcl procs by a string index * * @export * @interface TclProcHolder */ export interface TclProcHolder { [index: string]: TclProc; } export type ProcArgs = TclVariable[] | string[]; // Types of functions a proc can have export type TclProcFunction = | (( interpreter: Interpreter, args: ProcArgs, command: CommandToken, helpers: TclProcHelpers, ) => TclVariable) | (( interpreter: Interpreter, args: ProcArgs, command: CommandToken, helpers: TclProcHelpers, ) => Promise<TclVariable>); /** * The given options for a proc * * @interface TclProcOptions */ interface TclProcOptions { helpMessages: { [index: string]: string; }; arguments: { pattern: string; textOnly: boolean; simpleOnly: boolean; amount: | number | { start: number; end: number; }; }; } /** * This is the same as TclProcOptions, except you can leave options empty * * @export * @interface TclProcOptionsEmpty */ export interface TclProcOptionsEmpty { helpMessages?: { [index: string]: string; }; arguments?: { pattern?: string; textOnly?: boolean; simpleOnly?: boolean; amount?: | number | { start: number; end: number; }; }; } /** * The helper functions while running a proc * * @export * @interface TclProcHelpers */ export interface TclProcHelpers { sendHelp: (helpType: string) => never; solveExpression: (expression: string) => Promise<number>; } /** * This is the standard holder for a tcl procedure * * @export * @class TclProc */ export class TclProc { name: string; callback: TclProcFunction; options: TclProcOptions = { helpMessages: { wargs: `wrong # args`, wtype: `wrong type`, wexpression: `expression resolved to unusable value`, undefifop: `undefined if operation`, }, arguments: { amount: -1, pattern: `blank`, textOnly: false, simpleOnly: false, }, }; /** * Creates an instance of TclProc * Will assign the name and callback * * @param {string} name * @param {TclProcFunction} callback * @param {TclProcOptionsEmpty} [options] * @memberof TclProc */ constructor( name: string, callback: TclProcFunction, options?: TclProcOptionsEmpty, ) { this.name = name; this.callback = callback; // Set the options if (options) { if (options.helpMessages) this.options.helpMessages = { ...this.options.helpMessages, ...options.helpMessages, }; if (options.arguments) { if (options.arguments.amount) this.options.arguments.amount = options.arguments.amount; if (options.arguments.pattern) this.options.arguments.pattern = options.arguments.pattern; if (options.arguments.textOnly) this.options.arguments.textOnly = options.arguments.textOnly; if (options.arguments.textOnly || options.arguments.simpleOnly) this.options.arguments.simpleOnly = true; } } } }