Skip to content

Commit

Permalink
JS syntax check for browser
Browse files Browse the repository at this point in the history
  • Loading branch information
benchmarko committed Dec 8, 2024
1 parent 512f1dc commit 51f9745
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 9 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "locobasic",
"version": "0.1.9",
"version": "0.1.11",
"description": "# LocoBasic - Loco BASIC",
"type": "commonjs",
"scripts": {
Expand Down
19 changes: 16 additions & 3 deletions src/Core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export class Core implements ICore {

private vm = vm;

private onCheckSyntax = async (_s: string) => ""; // eslint-disable-line @typescript-eslint/no-unused-vars

public getConfigObject() {
return this.startConfig;
}
Expand All @@ -80,6 +82,10 @@ export class Core implements ICore {
vm.setOnCls(fn);
}

setOnCheckSyntax(fn: (s: string) => Promise<string>) {
this.onCheckSyntax = fn;
}

private arithmeticParser: Parser | undefined;

public compileScript(script: string) {
Expand All @@ -100,6 +106,12 @@ export class Core implements ICore {
}

let output: string;
output = await this.onCheckSyntax(compiledScript);
if (output) {
vm.cls();
return "ERROR: " + output;
}

try {
const fnScript = new Function("_o", compiledScript);
const result = fnScript(this.vm) || "";
Expand All @@ -111,17 +123,18 @@ export class Core implements ICore {
}

} catch (error) {
vm.cls();
output = "ERROR: ";
if (error instanceof Error) {
output += error.message;
output += String(error);

const anyErr = error as any;
const lineNumber = anyErr.lineNumber; // only on FireFox
const columnNumber = anyErr.columnNumber; // only on FireFox

if (lineNumber || columnNumber) {
const errLine = lineNumber - 2; // for some reason line 0 is 2
output += ` (line ${errLine}, column ${columnNumber})`;
const errLine = lineNumber - 2; // lineNumber -2 because of anonymous function added by new Function() constructor
output += ` (Line ${errLine}, column ${columnNumber})`;
}
} else {
output += "unknown";
Expand Down
4 changes: 3 additions & 1 deletion src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ export interface ICore {
compileScript(script: string): string,
executeScript(compiledScript: string): Promise<string>,
setOnCls(fn: () => void): void
setOnCheckSyntax(fn: (s: string) => Promise<string>): void
}

export interface IUi {
parseUri(urlQuery: string, config: ConfigType): string[],
onWindowLoad(event: Event): void,
setOutputText(value: string): void
setOutputText(value: string): void,
checkSyntax(str: string): Promise<string>
}
115 changes: 111 additions & 4 deletions src/Ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,55 @@

import { ICore, IUi, ConfigType } from "./Interfaces";

// Worker:
type PlainErrorEventType = {
lineno: number,
colno: number,
message: string
};

type ProcessingQueueType = {
resolve: (value: PlainErrorEventType) => void,
jsText: string
};

// based on: https://stackoverflow.com/questions/35252731/find-details-of-syntaxerror-thrown-by-javascript-new-function-constructor
// https://stackoverflow.com/a/55555357
const workerFn = () => {
const doEvalAndReply = (jsText: string) => {
self.addEventListener(
'error',
(errorEvent) => {
// Don't pollute the browser console:
errorEvent.preventDefault();
// The properties we want are actually getters on the prototype;
// they won't be retrieved when just stringifying so, extract them manually, and put them into a new object:
const { lineno, colno, message } = errorEvent;
const plainErrorEventObj: PlainErrorEventType = { lineno, colno, message };
self.postMessage(JSON.stringify(plainErrorEventObj));
},
{ once: true }
);
/* const fn = */ new Function("_o", jsText);
const plainErrorEventObj: PlainErrorEventType = {
lineno: -1,
colno: -1,
message: 'No Error: Parsing successful!'
};
self.postMessage(JSON.stringify(plainErrorEventObj));
};
self.addEventListener('message', (e) => {
doEvalAndReply(e.data);
});
};


export class Ui implements IUi {
private readonly core: ICore;
private basicCm: any;
private compiledCm: any;
//private static worker?: Worker;
private static getErrorEvent?: (s: string) => Promise<PlainErrorEventType>;

constructor(core: ICore) {
this.core = core;
Expand Down Expand Up @@ -52,7 +97,7 @@ export class Ui implements IUi {
this.setOutputText(this.getOutputText() + output + (output.endsWith("\n") ? "" : "\n"));
}

private oncompiledTextChange(_event: Event) { // eslint-disable-line @typescript-eslint/no-unused-vars
private onCompiledTextChange(_event: Event) { // eslint-disable-line @typescript-eslint/no-unused-vars
const autoExecuteInput = document.getElementById("autoExecuteInput") as HTMLInputElement;
if (autoExecuteInput.checked) {
const executeButton = window.document.getElementById("executeButton") as HTMLButtonElement;
Expand Down Expand Up @@ -107,7 +152,6 @@ export class Ui implements IUi {
}
}


private setExampleSelectOptions(examples: Record<string, string>) {
const exampleSelect = document.getElementById("exampleSelect") as HTMLSelectElement;

Expand All @@ -124,6 +168,69 @@ export class Ui implements IUi {
}
}

private static getErrorEventFn() {
if (Ui.getErrorEvent) {
return Ui.getErrorEvent;
}

const blob = new Blob(
[`(${workerFn})();`],
{ type: "text/javascript" }
);

const worker = new Worker(window.URL.createObjectURL(blob));
// Use a queue to ensure processNext only calls the worker once the worker is idle
const processingQueue: ProcessingQueueType[] = [];
let processing = false;

const processNext = () => {
processing = true;
const { resolve, jsText } = processingQueue.shift() as ProcessingQueueType;
worker.addEventListener(
'message',
({ data }) => {
resolve(JSON.parse(data));
if (processingQueue.length) {
processNext();
} else {
processing = false;
}
},
{ once: true }
);
worker.postMessage(jsText);
};

const getErrorEvent = (jsText: string) => new Promise<PlainErrorEventType>((resolve) => {
processingQueue.push({ resolve, jsText });
if (!processing) {
processNext();
}
});

Ui.getErrorEvent = getErrorEvent;
return getErrorEvent;
}

private static describeError(stringToEval: string, lineno: number, colno: number) {
const lines = stringToEval.split('\n');
const line = lines[lineno - 1];
return `${line}\n${' '.repeat(colno - 1) + '^'}`;
}

public async checkSyntax(str: string) {
const getErrorEvent = Ui.getErrorEventFn();

let output = "";
const { lineno, colno, message } = await getErrorEvent(str);
if (message === 'No Error: Parsing successful!') {
return "";
}
output += `Syntax error thrown at: Line ${lineno - 2}, col: ${colno}\n`; // lineNo -2 because of anonymous function added by new Function() constructor
output += Ui.describeError(str, lineno - 2, colno);
return output;
}

private fnDecodeUri(s: string) {
let decoded = "";

Expand Down Expand Up @@ -161,7 +268,7 @@ export class Ui implements IUi {
basicText.addEventListener('change', (event) => this.onbasicTextChange(event));

const compiledText = window.document.getElementById("compiledText") as HTMLTextAreaElement;
compiledText.addEventListener('change', (event) => this.oncompiledTextChange(event));
compiledText.addEventListener('change', (event) => this.onCompiledTextChange(event));

const compileButton = window.document.getElementById("compileButton") as HTMLButtonElement;
compileButton.addEventListener('click', (event) => this.onCompileButtonClick(event), false);
Expand All @@ -184,7 +291,7 @@ export class Ui implements IUi {
lineNumbers: true,
mode: 'javascript'
});
this.compiledCm.on('changes', this.debounce((event: Event) => this.oncompiledTextChange(event), "debounceExecute"));
this.compiledCm.on('changes', this.debounce((event: Event) => this.onCompiledTextChange(event), "debounceExecute"));
}

Ui.asyncDelay(() => {
Expand Down
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ if (typeof window !== "undefined") {
fnParseArgs(args, config);

core.setOnCls(() => ui.setOutputText(""));
core.setOnCheckSyntax((s: string) => Promise.resolve(ui.checkSyntax(s)));
ui.onWindowLoad(new Event("onload"));
}

Expand Down

0 comments on commit 51f9745

Please sign in to comment.