import { Ace } from "ace-builds"; import { TransferHandler } from "comlink"; import * as log from "log"; export function isSome(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 = { 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, map2: Map): 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 = new (...args: any[]) => T export const TypedEventTarget = < EventMap extends object, T extends Constructor >( superClass: T, ) => { class TypedEventTargetClass extends superClass { dispatchCustomEvent( type: K, data?: EventMap[K] extends CustomEvent ? 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> & T; } interface TypedEventTargetInterface extends EventTarget { addEventListener( type: K, callback: ( event: EventMap[K] extends Event ? EventMap[K] : never, ) => EventMap[K] extends Event ? void : never, options?: boolean | AddEventListenerOptions, ): void; removeEventListener( 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( type: K, data?: EventMap[K] extends CustomEvent ? DetailData : never, eventInitDict?: EventInit, ): boolean; } export function dispatchTypedEvent( target: TypedEventTargetInterface, type: K, data: EventMap[K] extends CustomEvent ? DetailData : never ): boolean { return target.dispatchEvent(new CustomEvent(type as string, { detail: data })) }