JS: A working v0.9.0
This commit is contained in:
parent
f614b8db20
commit
a1b8bc4630
5 changed files with 261 additions and 216 deletions
112
assets/js/command.js
Normal file
112
assets/js/command.js
Normal 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(", ")}] }`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
57
assets/js/commandExecutor.js
Normal file
57
assets/js/commandExecutor.js
Normal 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
32
assets/js/setHandler.js
Normal 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
54
assets/js/terms.js
Normal 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}]`;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue