Files
ic10emu/www/src/ts/utils.ts
2024-09-16 21:40:51 -07:00

432 lines
11 KiB
TypeScript

import { Ace } from "ace-builds";
import { TransferHandler } from "comlink";
import * as log from "log";
export function isSome<T>(object: T | null | undefined): object is T {
return typeof object !== "undefined" && object !== null;
}
export function range(size: number, start: number = 0): number[] {
const base = [...Array(size ?? 0).keys()]
if (start != 0) {
return base.map(i => i + start);
}
return base
}
export function docReady(fn: () => void) {
// see if DOM is already available
if (
document.readyState === "complete" ||
document.readyState === "interactive"
) {
setTimeout(fn, 1);
} else {
document.addEventListener("DOMContentLoaded", fn);
}
}
function isZeroNegative(zero: number) {
return Object.is(zero, -0);
}
function makeCRCTable() {
let c;
const crcTable = [];
for (let n = 0; n < 256; n++) {
c = n;
for (let k = 0; k < 8; k++) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
crcTable[n] = c;
}
return crcTable;
}
const crcTable = makeCRCTable();
// Signed crc32 for string
export function crc32(str: string): number {
let crc = 0 ^ -1;
for (let i = 0, len = str.length; i < len; i++) {
crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xff];
}
return crc ^ -1;
}
export function numberToString(n: number): string {
if (isZeroNegative(n)) return "-0";
return n?.toString() ?? `${n}`;
}
export function displayNumber(n: number): string {
return numberToString(n).replace("Infinity", "∞");
}
function replacer(_key: any, value: any) {
if (value instanceof Map) {
return {
dataType: "Map",
value: Array.from(value.entries()), // or with spread: value: [...value]
};
} else if (
typeof value === "number" &&
(!Number.isFinite(value) || Number.isNaN(value) || isZeroNegative(value))
) {
return {
dataType: "Number",
value: numberToString(value),
};
} else if (typeof value === "bigint") {
return {
dataType: "BigInt",
value: value.toString(),
}
} else if (typeof value === "undefined") {
return {
dataType: "undefined",
};
} else {
return value;
}
}
function reviver(_key: any, value: any) {
if (typeof value === "object" && value !== null) {
if (value.dataType === "Map") {
return new Map(value.value);
} else if (value.dataType === "Number") {
return parseFloat(value.value);
} else if (value.dataType === "BigInt") {
return BigInt(value.value);
} else if (value.dataType === "undefined") {
return undefined;
}
}
return value;
}
export function toJson(value: any): string {
return JSON.stringify(value, replacer);
}
export function fromJson(value: string): any {
return JSON.parse(value, reviver);
}
// this is a hack that *may* not be needed
type SuitableForSpecialJson = any;
export const comlinkSpecialJsonTransferHandler: TransferHandler<any, string> = {
canHandle: (obj: unknown): obj is SuitableForSpecialJson => {
return typeof obj === "object"
|| (
typeof obj === "number"
&& (!Number.isFinite(obj) || Number.isNaN(obj) || isZeroNegative(obj))
)
|| typeof obj === "undefined";
},
serialize: (obj: SuitableForSpecialJson) => {
const sJson = toJson(obj);
return [
sJson,
[],
]
},
deserialize: (obj: string) => fromJson(obj)
};
export function compareMaps(map1: Map<any, any>, map2: Map<any, any>): boolean {
let testVal;
if (map1.size !== map2.size) {
return false;
}
for (let [key, val] of map1) {
testVal = map2.get(key);
if ((testVal === undefined && !map2.has(key)) || !structuralEqual(val, testVal)) {
return false;
}
}
return true;
}
export function compareArrays(array1: any[], array2: any[]): boolean {
if (array1.length !== array2.length) {
return false;
}
for (let i = 0, len = array1.length; i < len; i++) {
if (!structuralEqual(array1[i], array2[i])) {
return false;
}
}
return true;
}
export function compareStructuralObjects(a: object, b: object): boolean {
const aProps = new Map(Object.entries(a));
const bProps = new Map(Object.entries(b));
return compareMaps(aProps, bProps);
}
export function structuralEqual(a: any, b: any): boolean {
const aType = typeof a;
const bType = typeof b;
if (aType !== typeof bType) {
return false;
}
if (a instanceof Map && b instanceof Map) {
return compareMaps(a, b);
}
if (Array.isArray(a) && Array.isArray(b)) {
return compareArrays(a, b);
}
if (aType === "object" && bType === "object") {
return compareStructuralObjects(a, b);
}
return a !== b;
}
// probably not needed, fetch() exists now
export function makeRequest(opts: {
method: string;
url: string;
headers: { [key: string]: string };
params: any;
}) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(opts.method, opts.url);
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject({
status: xhr.status,
statusText: xhr.statusText,
});
}
};
xhr.onerror = function () {
reject({
status: xhr.status,
statusText: xhr.statusText,
});
};
if (opts.headers) {
Object.keys(opts.headers).forEach(function (key) {
xhr.setRequestHeader(key, opts.headers[key]);
});
}
var params = opts.params;
if (params && typeof params === "object") {
params = Object.keys(params)
.map(function (key) {
return (
encodeURIComponent(key) + "=" + encodeURIComponent(params[key])
);
})
.join("&");
}
xhr.send(params);
});
}
export async function saveFile(content: BlobPart) {
const blob = new Blob([content], { type: "text/plain" });
if (typeof window.showSaveFilePicker !== "undefined") {
log.info("Saving via FileSystem API");
try {
const saveHandle = await window.showSaveFilePicker({
types: [
{
// suggestedName: "code.ic10",
description: "Text Files",
accept: {
"text/plain": [".txt", ".ic10"],
},
},
],
});
const ws = await saveHandle.createWritable();
await ws.write(blob);
await ws.close();
} catch (e) {
log.error(e);
}
} else {
log.info("saving file via hidden link event");
var a = document.createElement("a");
const date = new Date().valueOf().toString(16);
a.download = `code_${date}.ic10`;
a.href = window.URL.createObjectURL(blob);
a.click();
}
}
export async function openFile(editor: Ace.Editor) {
if (typeof window.showOpenFilePicker !== "undefined") {
log.info("opening file via FileSystem Api");
try {
const [fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
const contents = await file.text();
const session = editor.getSession();
session.setValue(contents);
} catch (e) {
log.error(e);
}
} else {
log.info("opening file via hidden input event");
let input = document.createElement("input");
input.type = "file";
input.accept = ".txt,.ic10,.mips,text/*";
input.onchange = (_) => {
const files = Array.from(input.files!);
log.trace(files);
const file = files[0];
var reader = new FileReader();
reader.onload = (e) => {
const contents = e.target!.result as string;
const session = editor.getSession();
// session.id = file.name;
session.setValue(contents);
};
reader.readAsText(file);
};
input.click();
}
}
export function parseNumber(s: string): number {
switch (s.toLowerCase()) {
case "nan":
return Number.NaN;
case "pinf":
return Number.POSITIVE_INFINITY;
case "ninf":
return Number.NEGATIVE_INFINITY;
case "pi":
return 3.141592653589793;
case "deg2rad":
return 0.0174532923847437;
case "rad2deg":
return 57.2957801818848;
case "epsilon":
return Number.EPSILON;
}
if (/^%[01]+$/.test(s)) {
return parseInt(s.slice(1), 2);
}
if (/^\$[0-9A-Fa-f]+$/.test(s)) {
return parseInt(s.slice(1), 16);
}
if (/[a-fA-F]/.test(s)) {
const hex = parseHex(s);
if (!isNaN(hex)) {
return hex;
}
}
s.replace("∞", "Infinity");
return parseFloat(s);
}
export function parseHex(h: string): number {
var val = parseInt(h, 16);
if (val.toString(16) === h.toLowerCase()) {
return val;
} else {
return NaN;
}
}
export function parseIntWithHexOrBinary(s: string): number {
if (/^%[01]+$/.test(s)) {
return parseInt(s.slice(1), 2);
}
if (/^\$[0-9A-Fa-f]+$/.test(s)) {
return parseInt(s.slice(1), 16);
}
if (/[a-fA-F]/.test(s)) {
const hex = parseHex(s);
if (!isNaN(hex)) {
return hex;
}
}
return parseInt(s);
}
export function clamp(val: number, min: number, max: number) {
return Math.min(Math.max(val, min), max);
}
type Constructor<T = {}> = new (...args: any[]) => T
export const TypedEventTarget = <
EventMap extends object,
T extends Constructor<EventTarget>
>(
superClass: T,
) => {
class TypedEventTargetClass extends superClass {
dispatchCustomEvent<K extends keyof EventMap>(
type: K,
data?: EventMap[K] extends CustomEvent<infer DetailData> ? DetailData : never,
eventInitDict?: EventInit,
): boolean {
return this.dispatchEvent(new CustomEvent(type as string, {
detail: data,
bubbles: eventInitDict?.bubbles,
cancelable: eventInitDict?.cancelable,
composed: eventInitDict?.composed
}))
}
}
return TypedEventTargetClass as Constructor<TypedEventTargetInterface<EventMap>> & T;
}
interface TypedEventTargetInterface<EventMap> extends EventTarget {
addEventListener<K extends keyof EventMap>(
type: K,
callback: (
event: EventMap[K] extends Event ? EventMap[K] : never,
) => EventMap[K] extends Event ? void : never,
options?: boolean | AddEventListenerOptions,
): void;
removeEventListener<K extends keyof EventMap>(
type: K,
callback: (
event: EventMap[K] extends Event ? EventMap[K] : never,
) => EventMap[K] extends Event ? void : never,
options?: boolean | AddEventListenerOptions,
): void;
/**
* @deprecated This method is untyped because the event name is not present in the event map
*/
addEventListener(
type: string,
callback: EventListenerOrEventListenerObject | null,
options?: EventListenerOptions | boolean,
): void;
/**
* @deprecated This method is untyped because the event name is not present in the event map
*/
removeEventListener(
type: string,
callback: EventListenerOrEventListenerObject | null,
options?: EventListenerOptions | boolean,
): void;
dispatchCustomEvent<K extends keyof EventMap>(
type: K,
data?: EventMap[K] extends CustomEvent<infer DetailData> ? DetailData : never,
eventInitDict?: EventInit,
): boolean;
}
export function dispatchTypedEvent<EventMap, K extends keyof EventMap>(
target: TypedEventTargetInterface<EventMap>,
type: K,
data: EventMap[K] extends CustomEvent<infer DetailData> ? DetailData : never
): boolean {
return target.dispatchEvent(new CustomEvent(type as string, { detail: data }))
}