From a1b8bc46301ab8c67401ebb736b98fced4fbf981 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 7 Aug 2024 08:05:59 -1000 Subject: [PATCH] JS: A working v0.9.0 --- assets/js/command.js | 112 ++++++++++ assets/js/{command_bar.js => commandBar.js} | 222 +------------------- assets/js/commandExecutor.js | 57 +++++ assets/js/setHandler.js | 32 +++ assets/js/terms.js | 54 +++++ 5 files changed, 261 insertions(+), 216 deletions(-) create mode 100644 assets/js/command.js rename assets/js/{command_bar.js => commandBar.js} (61%) create mode 100644 assets/js/commandExecutor.js create mode 100644 assets/js/setHandler.js create mode 100644 assets/js/terms.js diff --git a/assets/js/command.js b/assets/js/command.js new file mode 100644 index 0000000..5c7a30b --- /dev/null +++ b/assets/js/command.js @@ -0,0 +1,112 @@ +import { NameTerm, NumberTerm } from "./terms.js"; + +class ParseCommandError extends SyntaxError { } + +class Parameter { + name; + value; + + constructor(name, value) { + if (!name instanceof NameTerm) { + throw new TypeError("Parameter name must be a NameTerm"); + } + + this.name = name; + this.value = value; + } + + toString() { + return this.value ? `${this.name}=${this.value}` : `${this.name}`; + } + + valueOf() { + if (this.value !== undefined) { + return this.value.valueOf(); + } else { + return this.name; + } + } +} + +export class Command { + static stringRegex = /'[^'"]*'|"[^'"]*"/; + static whitespaceRegex = /\s+/; + + static parse(commandString) { + const terms = commandString.split(Command.whitespaceRegex).filter(s => !!s); + + if (terms.length < 1) { + return null; + } + + let verb; + try { + verb = new NameTerm(terms[0]); + } catch { + throw new ParseCommandError(`Invalid verb: ${verb}`); + } + + const parameters = terms.slice(1).map(t => { + const [nameString, valueString, ...rest] = t.split("="); + + if (rest.length > 0) { + throw new ParseCommandError(`Invalid parameter: ${t}`); + } + + let name; + try { + name = new NameTerm(nameString); + } catch { + throw new ParseCommandError(`Invalid parameter name: ${t}`); + } + + let value; + if (valueString) { + for (const termClass of [NameTerm, NumberTerm]) { + try { + value = new termClass(valueString); + break; + } catch { + continue; + } + } + + if (!value) { + throw new ParseCommandError(`Invalid parameter value: ${t}`); + } + } + + return new Parameter(name, value); + }); + + return new Command(commandString, verb, parameters); + } + + originalString; + verb; + parameters = [] + + constructor(originalString, verb, parameters) { + if (!verb instanceof String) { + throw new TypeError("Command verb must be a string"); + } + + this.originalString = originalString; + this.verb = verb; + this.parameters = parameters; + } + + parameter(nameOrIndex) { + if (typeof nameOrIndex === "string" || nameOrIndex instanceof String) { + return this.parameters.find(p => p.name == nameOrIndex); + } else if (nameOrIndex instanceof Number) { + return this.parameters[nameOrIndex].valueOf(); + } + + return null; + } + + toString() { + return `Command: {verb: "${this.verb.toString()}", parameters: [${this.parameters.join(", ")}] }`; + } +} diff --git a/assets/js/command_bar.js b/assets/js/commandBar.js similarity index 61% rename from assets/js/command_bar.js rename to assets/js/commandBar.js index fc37e3f..f338711 100644 --- a/assets/js/command_bar.js +++ b/assets/js/commandBar.js @@ -1,3 +1,6 @@ +import { Command } from "./command.js" +import { CommandExecutor } from "./commandExecutor.js"; + class CommandBar extends HTMLElement { static observedAttributes = [ "presented" @@ -11,13 +14,14 @@ class CommandBar extends HTMLElement { #leader = ":"; - #commandExecutor = new CommandExecutor(); + #commandExecutor; #indexOfLastShownCommandFromHistory = null; #shouldPresentAfterConnection = false; constructor() { super(); - this.#shadowRoot = this.attachShadow({ mode: "closed" }); + this.#commandExecutor = new CommandExecutor(this); + this.#shadowRoot = this.attachShadow({ mode: "open" }); } get #elementHasPresentedAttribute() { @@ -260,218 +264,4 @@ class CommandBar extends HTMLElement { } } -class CommandExecutor { - #history = []; - handlers = new Map(); - - addHandler(handler) { - this.handlers.set(handler.name, handler); - } - - executeCommand(command) { - this.appendCommandToHistory(command); - - const handler = this.#handlerForCommand(command); - if (!handler) { - throw new Error(`No handler for '${command.verb.valueOf()}' command`); - } - - let result; - try { - result = handler.handler(command); - } catch (e) { - result = null; - } - - if (result !== undefined && !result) { - console.error(`Command failed: ${command}`); - } - } - - #handlerForCommand(command) { - const verb = command.verb.valueOf(); - return this.handlers.get(verb); - } - - // MARK: History - - get historyLength() { - return this.#history.length; - } - - appendCommandToHistory(command) { - this.#history.push(command); - } - - commandAtHistoryIndex(index) { - return this.#history[index]; - } -} - -class ParseCommandError extends SyntaxError { } - -class Parameter { - name; - value; - - constructor(name, value) { - if (!name instanceof NameTerm) { - throw new TypeError("Parameter name must be a NameTerm"); - } - - this.name = name; - this.value = value; - } - - toString() { - return this.value ? `${this.name}=${this.value}` : `${this.name}`; - } - - valueOf() { - if (this.value !== undefined) { - return this.value.valueOf(); - } else { - return this.name; - } - } -} - -class Term { - stringValue; - - constructor(stringValue) { - this.stringValue = stringValue; - } - - toString() { - return `Term[${stringValue}]`; - } - - valueOf() { - return this.stringValue; - } -} - -class NumberTerm extends Term { - value; - - constructor(stringValue) { - super(stringValue); - - const parsedValue = parseInt(stringValue); - if (isNaN(parsedValue)) { - throw new TypeError("Number value is not a valid number"); - } - - this.value = parsedValue; - } - - valueOf() { - return this.value; - } - - toString() { - return `Number[${this.value}]`; - } -} - -class NameTerm extends Term { - static regex = /[-\w]+/; - - constructor(stringValue) { - if (!NameTerm.regex.test(stringValue)) { - throw new TypeError("Name must be string with non-zero length"); - } - - super(stringValue); - } - - toString() { - return `Name[${this.stringValue}]`; - } -} - -class Command { - static stringRegex = /'[^'"]*'|"[^'"]*"/; - static whitespaceRegex = /\s+/; - - static parse(commandString) { - const terms = commandString.split(Command.whitespaceRegex).filter(s => !!s); - - if (terms.length < 1) { - return null; - } - - let verb; - try { - verb = new NameTerm(terms[0]); - } catch { - throw new ParseCommandError(`Invalid verb: ${verb}`); - } - - const parameters = terms.slice(1).map(t => { - const [nameString, valueString, ...rest] = t.split("="); - - if (rest.length > 0) { - throw new ParseCommandError(`Invalid parameter: ${t}`); - } - - let name; - try { - name = new NameTerm(nameString); - } catch { - throw new ParseCommandError(`Invalid parameter name: ${t}`); - } - - let value; - if (valueString) { - for (const termClass of [NameTerm, NumberTerm]) { - try { - value = new termClass(valueString); - break; - } catch { - continue; - } - } - - if (!value) { - throw new ParseCommandError(`Invalid parameter value: ${t}`); - } - } - - return new Parameter(name, value); - }); - - return new Command(commandString, verb, parameters); - } - - originalString; - verb; - parameters = [] - - constructor(originalString, verb, parameters) { - if (!verb instanceof String) { - throw new TypeError("Command verb must be a string"); - } - - this.originalString = originalString; - this.verb = verb; - this.parameters = parameters; - } - - parameter(nameOrIndex) { - if (typeof nameOrIndex === "string" || nameOrIndex instanceof String) { - return this.parameters.find(p => p.name == nameOrIndex); - } else if (nameOrIndex instanceof Number) { - return this.parameters[nameOrIndex].valueOf(); - } - - return null; - } - - toString() { - return `Command: {verb: "${this.verb.toString()}", parameters: [${this.parameters.join(", ")}] }`; - } -} - window.customElements.define("command-bar", CommandBar); diff --git a/assets/js/commandExecutor.js b/assets/js/commandExecutor.js new file mode 100644 index 0000000..f123f44 --- /dev/null +++ b/assets/js/commandExecutor.js @@ -0,0 +1,57 @@ +import { SetOptionHandler } from "./setHandler.js"; + +export class CommandExecutor { + commandBar; + #history = []; + #handlers = new Map(); + + constructor(commandBar) { + this.commandBar = commandBar; + console.log(this.#handlers); + this.addHandler(new SetOptionHandler()); + console.log(this.#handlers); + } + + addHandler(handler) { + this.#handlers.set(handler.name, handler); + } + + executeCommand(command) { + this.appendCommandToHistory(command); + + const handler = this.#handlerForCommand(command); + if (!handler) { + throw new Error(`No handler for '${command.verb.valueOf()}' command`); + } + + let result; + try { + result = handler.execute(command, this.commandBar); + } catch (e) { + result = null; + } + + if (result !== undefined && !result) { + console.error(`Command failed: ${command}`); + } + } + + #handlerForCommand(command) { + const verb = command.verb.valueOf(); + return this.#handlers.get(verb); + } + + // MARK: History + + get historyLength() { + return this.#history.length; + } + + appendCommandToHistory(command) { + this.#history.push(command); + } + + commandAtHistoryIndex(index) { + return this.#history[index]; + } +} diff --git a/assets/js/setHandler.js b/assets/js/setHandler.js new file mode 100644 index 0000000..b78a47c --- /dev/null +++ b/assets/js/setHandler.js @@ -0,0 +1,32 @@ +class SetEvent extends Event { + #option; + #newValue; + + constructor(option, newValue) { + super("setOption") + this.#option = option; + this.#newValue = newValue; + } + + get option() { + return this.#option; + } + + get newValue() { + return this.#newValue; + } +} + +export class SetOptionHandler { + get name() { + return "set"; + } + + execute(command, commandBar) { + for (const param of command.parameters) { + const setEvent = new SetEvent(param.name.valueOf(), param.value.valueOf()); + commandBar.dispatchEvent(setEvent); + } + return true; + } +} diff --git a/assets/js/terms.js b/assets/js/terms.js new file mode 100644 index 0000000..064902c --- /dev/null +++ b/assets/js/terms.js @@ -0,0 +1,54 @@ +class Term { + stringValue; + + constructor(stringValue) { + this.stringValue = stringValue; + } + + toString() { + return `Term[${stringValue}]`; + } + + valueOf() { + return this.stringValue; + } +} + +export class NumberTerm extends Term { + value; + + constructor(stringValue) { + super(stringValue); + + const parsedValue = parseInt(stringValue); + if (isNaN(parsedValue)) { + throw new TypeError("Number value is not a valid number"); + } + + this.value = parsedValue; + } + + valueOf() { + return this.value; + } + + toString() { + return `Number[${this.value}]`; + } +} + +export class NameTerm extends Term { + static regex = /[-\w]+/; + + constructor(stringValue) { + if (!NameTerm.regex.test(stringValue)) { + throw new TypeError("Name must be string with non-zero length"); + } + + super(stringValue); + } + + toString() { + return `Name[${this.stringValue}]`; + } +}