JS: A working v0.9.0

This commit is contained in:
Eryn Wells 2024-08-07 08:05:59 -10:00
parent f614b8db20
commit a1b8bc4630
5 changed files with 261 additions and 216 deletions

112
assets/js/command.js Normal file
View file

@ -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(", ")}] }`;
}
}

View file

@ -1,3 +1,6 @@
import { Command } from "./command.js"
import { CommandExecutor } from "./commandExecutor.js";
class CommandBar extends HTMLElement { class CommandBar extends HTMLElement {
static observedAttributes = [ static observedAttributes = [
"presented" "presented"
@ -11,13 +14,14 @@ class CommandBar extends HTMLElement {
#leader = ":"; #leader = ":";
#commandExecutor = new CommandExecutor(); #commandExecutor;
#indexOfLastShownCommandFromHistory = null; #indexOfLastShownCommandFromHistory = null;
#shouldPresentAfterConnection = false; #shouldPresentAfterConnection = false;
constructor() { constructor() {
super(); super();
this.#shadowRoot = this.attachShadow({ mode: "closed" }); this.#commandExecutor = new CommandExecutor(this);
this.#shadowRoot = this.attachShadow({ mode: "open" });
} }
get #elementHasPresentedAttribute() { 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); window.customElements.define("command-bar", CommandBar);

View file

@ -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];
}
}

32
assets/js/setHandler.js Normal file
View file

@ -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;
}
}

54
assets/js/terms.js Normal file
View file

@ -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}]`;
}
}