From fa03b2e98cb6dce518c722e3fa6e0b2427009686 Mon Sep 17 00:00:00 2001 From: fnky Date: Mon, 10 Jan 2022 02:45:07 +0100 Subject: [PATCH] Improve type-safety for Grecha tags --- js/eval.js | 16 ++--- js/grecha.js | 35 +++++----- js/index.js | 42 ++++++------ package-lock.json | 6 +- package.json | 2 +- ts/grecha.ts | 160 +++++++++++++++++++++++++++------------------- ts/index.ts | 36 +++++------ 7 files changed, 163 insertions(+), 134 deletions(-) diff --git a/js/eval.js b/js/eval.js index 9a9883d..79d9291 100644 --- a/js/eval.js +++ b/js/eval.js @@ -81,7 +81,7 @@ function parse_primary(lexer) { var expr = parse_expr(lexer); token = lexer.next(); if (token !== ')') { - throw new Error("Expected ')' but got '" + token + "'"); + throw new Error("Expected ')' but got '".concat(token, "'")); } return expr; } @@ -113,7 +113,7 @@ function parse_primary(lexer) { next_token = lexer.next(); } if (next_token !== ')') { - throw Error("Expected ')' but got '" + next_token + "'"); + throw Error("Expected ')' but got '".concat(next_token, "'")); } return { "kind": "funcall", @@ -172,7 +172,7 @@ function compile_expr(src) { if (token !== null) { console.log(typeof (token)); console.log(token); - throw new Error("Unexpected token '" + token + "'"); + throw new Error("Unexpected token '".concat(token, "'")); } return result; } @@ -189,7 +189,7 @@ function run_expr(expr, user_context) { if (user_context.vars && value in user_context.vars) { return user_context.vars[value]; } - throw new Error("Unknown variable '" + value + "'"); + throw new Error("Unknown variable '".concat(value, "'")); } else { return number; @@ -200,24 +200,24 @@ function run_expr(expr, user_context) { if (unary_op.op in UNARY_OPS) { return UNARY_OPS[unary_op.op](run_expr(unary_op.operand, user_context)); } - throw new Error("Unknown unary operator '" + unary_op.op + "'"); + throw new Error("Unknown unary operator '".concat(unary_op.op, "'")); } case 'binary_op': { var binary_op = expr.payload; if (binary_op.op in BINARY_OPS) { return BINARY_OPS[binary_op.op].func(run_expr(binary_op.lhs, user_context), run_expr(binary_op.rhs, user_context)); } - throw new Error("Unknown binary operator '" + binary_op.op + "'"); + throw new Error("Unknown binary operator '".concat(binary_op.op, "'")); } case 'funcall': { var funcall = expr.payload; if (user_context.funcs && funcall.name in user_context.funcs) { return (_a = user_context.funcs)[funcall.name].apply(_a, funcall.args.map(function (arg) { return run_expr(arg, user_context); })); } - throw new Error("Unknown function '" + funcall.name + "'"); + throw new Error("Unknown function '".concat(funcall.name, "'")); } default: { - throw new Error("Unexpected AST node '" + expr.kind + "'"); + throw new Error("Unexpected AST node '".concat(expr.kind, "'")); } } } diff --git a/js/grecha.js b/js/grecha.js index 879ca3c..d31993d 100644 --- a/js/grecha.js +++ b/js/grecha.js @@ -1,8 +1,12 @@ "use strict"; -var __spreadArray = (this && this.__spreadArray) || function (to, from) { - for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) - to[j] = from[i]; - return to; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); }; var LOREM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; function tag(name) { @@ -13,7 +17,7 @@ function tag(name) { var result = document.createElement(name); for (var _a = 0, children_1 = children; _a < children_1.length; _a++) { var child = children_1[_a]; - if (typeof (child) === 'string') { + if (typeof child === 'string') { result.appendChild(document.createTextNode(child)); } else { @@ -35,63 +39,63 @@ function canvas() { for (var _i = 0; _i < arguments.length; _i++) { children[_i] = arguments[_i]; } - return tag.apply(void 0, __spreadArray(["canvas"], children)); + return tag.apply(void 0, __spreadArray(["canvas"], children, false)); } function h1() { var children = []; for (var _i = 0; _i < arguments.length; _i++) { children[_i] = arguments[_i]; } - return tag.apply(void 0, __spreadArray(["h1"], children)); + return tag.apply(void 0, __spreadArray(["h1"], children, false)); } function h2() { var children = []; for (var _i = 0; _i < arguments.length; _i++) { children[_i] = arguments[_i]; } - return tag.apply(void 0, __spreadArray(["h2"], children)); + return tag.apply(void 0, __spreadArray(["h2"], children, false)); } function h3() { var children = []; for (var _i = 0; _i < arguments.length; _i++) { children[_i] = arguments[_i]; } - return tag.apply(void 0, __spreadArray(["h3"], children)); + return tag.apply(void 0, __spreadArray(["h3"], children, false)); } function p() { var children = []; for (var _i = 0; _i < arguments.length; _i++) { children[_i] = arguments[_i]; } - return tag.apply(void 0, __spreadArray(["p"], children)); + return tag.apply(void 0, __spreadArray(["p"], children, false)); } function a() { var children = []; for (var _i = 0; _i < arguments.length; _i++) { children[_i] = arguments[_i]; } - return tag.apply(void 0, __spreadArray(["a"], children)); + return tag.apply(void 0, __spreadArray(["a"], children, false)); } function div() { var children = []; for (var _i = 0; _i < arguments.length; _i++) { children[_i] = arguments[_i]; } - return tag.apply(void 0, __spreadArray(["div"], children)); + return tag.apply(void 0, __spreadArray(["div"], children, false)); } function span() { var children = []; for (var _i = 0; _i < arguments.length; _i++) { children[_i] = arguments[_i]; } - return tag.apply(void 0, __spreadArray(["span"], children)); + return tag.apply(void 0, __spreadArray(["span"], children, false)); } function select() { var children = []; for (var _i = 0; _i < arguments.length; _i++) { children[_i] = arguments[_i]; } - return tag.apply(void 0, __spreadArray(["select"], children)); + return tag.apply(void 0, __spreadArray(["select"], children, false)); } function img(src) { return tag("img").att$("src", src); @@ -117,8 +121,7 @@ function router(routes) { result.appendChild(routes[hashLocation]); return result; } - ; syncHash(); - window.addEventListener("hashchange", syncHash); + window.addEventListener('hashchange', syncHash); return result; } diff --git a/js/index.js b/js/index.js index de6ae23..f8eaabc 100644 --- a/js/index.js +++ b/js/index.js @@ -25,7 +25,7 @@ function compileShaderSource(gl, source, shaderType) { gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { - throw new Error("Could not compile " + shaderTypeToString() + " shader: " + gl.getShaderInfoLog(shader)); + throw new Error("Could not compile ".concat(shaderTypeToString(), " shader: ").concat(gl.getShaderInfoLog(shader))); } return shader; } @@ -43,7 +43,7 @@ function linkShaderProgram(gl, shaders, vertexAttribs) { } gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { - throw new Error("Could not link shader program: " + gl.getProgramInfoLog(program)); + throw new Error("Could not link shader program: ".concat(gl.getProgramInfoLog(program))); } return program; } @@ -77,7 +77,7 @@ function loadFilterProgram(gl, filter, vertexAttribs) { var paramsInputs = {}; var _loop_1 = function (paramName) { if (paramName in uniforms) { - throw new Error("Redefinition of existing uniform parameter " + paramName); + throw new Error("Redefinition of existing uniform parameter ".concat(paramName)); } switch (filter.params[paramName].type) { case "float": @@ -85,28 +85,28 @@ function loadFilterProgram(gl, filter, vertexAttribs) { var valuePreview_1 = span(filter.params[paramName].init.toString()); var valueInput = input("range"); if (filter.params[paramName].min !== undefined) { - valueInput.att$("min", filter.params[paramName].min); + valueInput.att$("min", filter.params[paramName].min.toString()); } if (filter.params[paramName].max !== undefined) { - valueInput.att$("max", filter.params[paramName].max); + valueInput.att$("max", filter.params[paramName].max.toString()); } if (filter.params[paramName].step !== undefined) { - valueInput.att$("step", filter.params[paramName].step); + valueInput.att$("step", filter.params[paramName].step.toString()); } if (filter.params[paramName].init !== undefined) { - valueInput.att$("value", filter.params[paramName].init); + valueInput.att$("value", filter.params[paramName].init.toString()); } paramsInputs[paramName] = valueInput; - valueInput.oninput = function () { - valuePreview_1.innerText = this.value; + valueInput.oninput = function (e) { + valuePreview_1.innerText = e.currentTarget.value; paramsPanel.dispatchEvent(new CustomEvent("paramsChanged")); }; var label = (_a = filter.params[paramName].label) !== null && _a !== void 0 ? _a : paramName; - paramsPanel.appendChild(div(span(label + ": "), valuePreview_1, div(valueInput))); + paramsPanel.appendChild(div(span("".concat(label, ": ")), valuePreview_1, div(valueInput))); } break; default: { - throw new Error("Filter parameters do not support type " + filter.params[paramName].type); + throw new Error("Filter parameters do not support type ".concat(filter.params[paramName].type)); } } uniforms[paramName] = gl.getUniformLocation(id, paramName); @@ -136,7 +136,7 @@ function ImageSelector() { var imageInput = input("file"); var imagePreview = img("img/tsodinClown.png") .att$("class", "widget-element") - .att$("width", CANVAS_WIDTH); + .att$("width", String(CANVAS_WIDTH)); var root = div(div(imageInput).att$("class", "widget-element"), imagePreview).att$("class", "widget"); root.selectedImage$ = function () { return imagePreview; @@ -155,7 +155,7 @@ function ImageSelector() { }; root.updateFiles$ = function (files) { imageInput.files = files; - imageInput.onchange(); + imageInput.dispatchEvent(new UIEvent("change", { view: window, bubbles: true })); }; imagePreview.addEventListener('load', function () { root.dispatchEvent(new CustomEvent("imageSelected", { @@ -168,8 +168,8 @@ function ImageSelector() { imageInput.value = ''; this.src = 'img/error.png'; }); - imageInput.onchange = function () { - imagePreview.src = URL.createObjectURL(this.files[0]); + imageInput.onchange = function (e) { + imagePreview.src = URL.createObjectURL(e.currentTarget.files[0]); }; return root; } @@ -196,15 +196,15 @@ function FilterList() { if (e.deltaY > 0) { root.selectedIndex = Math.min(root.selectedIndex + 1, root.length - 1); } - root.onchange(); + root.dispatchEvent(new UIEvent("change", { view: window, bubbles: true })); }); return root; } function FilterSelector() { var filterList_ = FilterList(); var filterPreview = canvas() - .att$("width", CANVAS_WIDTH) - .att$("height", CANVAS_HEIGHT); + .att$("width", String(CANVAS_WIDTH)) + .att$("height", String(CANVAS_HEIGHT)); var root = div(div("Filter: ", filterList_) .att$("class", "widget-element"), filterPreview.att$("class", "widget-element")).att$("class", "widget"); var gl = filterPreview.getContext("webgl", { antialias: false, alpha: false }); @@ -348,7 +348,7 @@ function FilterSelector() { delay: dt * 1000, dispose: 2, }); - renderProgress.style.width = (t / duration) * 50 + "%"; + renderProgress.style.width = "".concat((t / duration) * 50, "%"); t += dt; } gif.on('finished', function (blob) { @@ -360,7 +360,7 @@ function FilterSelector() { renderSpinner.style.display = "none"; }); gif.on('progress', function (p) { - renderProgress.style.width = 50 + p * 50 + "%"; + renderProgress.style.width = "".concat(50 + p * 50, "%"); }); gif.render(); return gif; @@ -416,6 +416,6 @@ window.onload = function () { gif.abort(); } var fileName = imageSelector.selectedFileName$(); - gif = filterSelector.render$(fileName + ".gif"); + gif = filterSelector.render$("".concat(fileName, ".gif")); }; }; diff --git a/package-lock.json b/package-lock.json index 3c6686b..570831f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,9 @@ "dev": true }, "typescript": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.2.tgz", - "integrity": "sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", + "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", "dev": true } } diff --git a/package.json b/package.json index e50c61a..f8b697a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,6 @@ "homepage": "https://github.com/tsoding/emoteJAM#readme", "devDependencies": { "@types/gif.js": "^0.2.1", - "typescript": "^4.3.2" + "typescript": "^4.5.4" } } diff --git a/ts/grecha.ts b/ts/grecha.ts index dcef8d2..15ed6d2 100644 --- a/ts/grecha.ts +++ b/ts/grecha.ts @@ -1,108 +1,134 @@ const LOREM: string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; +interface IntrinsicElements { + canvas: HTMLCanvasElement, + h1: HTMLHeadingElement, + h2: HTMLHeadingElement, + h3: HTMLHeadingElement, + p: HTMLParagraphElement, + a: HTMLAnchorElement, + div: HTMLDivElement, + span: HTMLSpanElement, + img: HTMLImageElement, + input: HTMLInputElement, + select: HTMLSelectElement, +} + +type ElementType

= { + [K in keyof IntrinsicElements]: P extends IntrinsicElements[K] ? K : never; +}[keyof IntrinsicElements]; + type Child = string | HTMLElement; -// TODO(#73): make tag more typesafe -// Essentially get rid of the `any` -type Tag = any; - -function tag(name: string, ...children: Child[]): Tag { - const result: Tag = document.createElement(name); - for (const child of children) { - if (typeof(child) === 'string') { - result.appendChild(document.createTextNode(child)); - } else { - result.appendChild(child); - } +type Tag = TElement & { + att$(this: Tag, name: string, value: string): Tag, + onclick$( + this: Tag, + callback: (this: GlobalEventHandlers, ev: MouseEvent) => Tag + ): Tag, + [handler: `${string}$`]: (...args: any[]) => any; +}; + +function tag( + name: T, + ...children: Child[] +): Tag { + const result = document.createElement(name as string) as Tag; + for (const child of children) { + if (typeof child === 'string') { + result.appendChild(document.createTextNode(child)); + } else { + result.appendChild(child); } + } - result.att$ = function(name: string, value: string) { - this.setAttribute(name, value); - return this; - }; + result.att$ = function (name, value) { + this.setAttribute(name, value); + return this; + }; + result.onclick$ = function (callback) { + this.onclick = callback; + return this; + }; - result.onclick$ = function(callback: (this: GlobalEventHandlers, ev: MouseEvent) => Tag) { - this.onclick = callback; - return this; - }; - - return result; + return result; } -function canvas(...children: Child[]): Tag { - return tag("canvas", ...children); +function canvas(...children: Child[]) { + return tag("canvas", ...children); } -function h1(...children: Child[]): Tag { - return tag("h1", ...children); +function h1(...children: Child[]) { + return tag("h1", ...children); } -function h2(...children: Child[]): Tag { - return tag("h2", ...children); +function h2(...children: Child[]) { + return tag("h2", ...children); } -function h3(...children: Child[]): Tag { - return tag("h3", ...children); +function h3(...children: Child[]) { + return tag("h3", ...children); } -function p(...children: Child[]): Tag { - return tag("p", ...children); +function p(...children: Child[]) { + return tag("p", ...children); } -function a(...children: Child[]): Tag { - return tag("a", ...children); +function a(...children: Child[]) { + return tag("a", ...children); } -function div(...children: Child[]): Tag { - return tag("div", ...children); +function div(...children: Child[]) { + return tag("div", ...children); } -function span(...children: Child[]): Tag { - return tag("span", ...children); +function span(...children: Child[]) { + return tag("span", ...children); } -function select(...children: Child[]): Tag { - return tag("select", ...children); +function select(...children: Child[]) { + return tag("select", ...children); } - -function img(src: string): Tag { - return tag("img").att$("src", src); +function img(src: string) { + return tag("img").att$("src", src); } -function input(type: string): Tag { - return tag("input").att$("type", type); +function input(type: string) { + return tag("input").att$("type", type); } interface Routes { - [route: string]: Tag + [route: string]: Tag } -function router(routes: Routes): Tag { - let result = div(); +function router(routes: Routes) { + let result = div(); - function syncHash() { - let hashLocation = document.location.hash.split('#')[1]; - if (!hashLocation) { - hashLocation = '/'; - } + function syncHash() { + let hashLocation = document.location.hash.split('#')[1]; + if (!hashLocation) { + hashLocation = '/'; + } - if (!(hashLocation in routes)) { - const route404 = '/404'; - console.assert(route404 in routes); - hashLocation = route404; - } + if (!(hashLocation in routes)) { + const route404 = '/404'; + console.assert(route404 in routes); + hashLocation = route404; + } - while (result.firstChild) { - result.removeChild(result.lastChild); - } - result.appendChild(routes[hashLocation]); + while (result.firstChild) { + // Type-safety: `lastChild` can never be `null` if `firstChild` is present, + // since it will only be `null` if `result` has no child elements. + result.removeChild(result.lastChild!); + } + result.appendChild(routes[hashLocation]); - return result; - }; + return result; + } - syncHash(); - window.addEventListener("hashchange", syncHash); + syncHash(); + window.addEventListener('hashchange', syncHash); - return result; + return result; } diff --git a/ts/index.ts b/ts/index.ts index 4c8abf2..b911d88 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -118,7 +118,7 @@ function loadFilterProgram(gl: WebGLRenderingContext, filter: Filter, vertexAttr // TODO(#55): there no "reset to default" button in the params panel of a filter let paramsPanel = div().att$("class", "widget-element"); - let paramsInputs: {[name: string]: Tag} = {}; + let paramsInputs: {[name: string]: Tag} = {}; for (let paramName in filter.params) { if (paramName in uniforms) { @@ -131,25 +131,25 @@ function loadFilterProgram(gl: WebGLRenderingContext, filter: Filter, vertexAttr const valueInput = input("range"); if (filter.params[paramName].min !== undefined) { - valueInput.att$("min", filter.params[paramName].min); + valueInput.att$("min", filter.params[paramName].min!.toString()); } if (filter.params[paramName].max !== undefined) { - valueInput.att$("max", filter.params[paramName].max); + valueInput.att$("max", filter.params[paramName].max!.toString()); } if (filter.params[paramName].step !== undefined) { - valueInput.att$("step", filter.params[paramName].step); + valueInput.att$("step", filter.params[paramName].step!.toString()); } if (filter.params[paramName].init !== undefined) { - valueInput.att$("value", filter.params[paramName].init); + valueInput.att$("value", filter.params[paramName].init!.toString()); } paramsInputs[paramName] = valueInput; - valueInput.oninput = function () { - valuePreview.innerText = this.value; + valueInput.oninput = function (e) { + valuePreview.innerText = (e.currentTarget as HTMLInputElement).value; paramsPanel.dispatchEvent(new CustomEvent("paramsChanged")); }; @@ -194,7 +194,7 @@ function ImageSelector() { const imageInput = input("file"); const imagePreview = img("img/tsodinClown.png") .att$("class", "widget-element") - .att$("width", CANVAS_WIDTH); + .att$("width", String(CANVAS_WIDTH)); const root = div( div(imageInput).att$("class", "widget-element"), imagePreview @@ -213,13 +213,13 @@ function ImageSelector() { } } - const file = imageInput.files[0]; + const file = imageInput.files![0]; return file ? removeFileNameExt(file.name) : 'result'; }; root.updateFiles$ = function(files: FileList) { imageInput.files = files; - imageInput.onchange(); + imageInput.dispatchEvent(new UIEvent("change", { view: window, bubbles: true })); } imagePreview.addEventListener('load', function(this: HTMLImageElement) { @@ -235,8 +235,8 @@ function ImageSelector() { this.src = 'img/error.png'; }); - imageInput.onchange = function() { - imagePreview.src = URL.createObjectURL(this.files[0]); + imageInput.onchange = function(e) { + imagePreview.src = URL.createObjectURL((e.currentTarget as HTMLInputElement).files![0]); }; return root; @@ -270,7 +270,7 @@ function FilterList() { if (e.deltaY > 0) { root.selectedIndex = Math.min(root.selectedIndex + 1, root.length - 1); } - root.onchange(); + root.dispatchEvent(new UIEvent("change", { view: window, bubbles: true })); }); return root; @@ -279,8 +279,8 @@ function FilterList() { function FilterSelector() { const filterList_ = FilterList(); const filterPreview = canvas() - .att$("width", CANVAS_WIDTH) - .att$("height", CANVAS_HEIGHT); + .att$("width", String(CANVAS_WIDTH)) + .att$("height", String(CANVAS_HEIGHT)); const root = div( div("Filter: ", filterList_) .att$("class", "widget-element"), @@ -337,7 +337,7 @@ function FilterSelector() { if (program) { const snapshot = program.paramsPanel.paramsSnapshot$(); for (let paramName in snapshot) { - gl.uniform1f(snapshot[paramName].uniform, snapshot[paramName].value); + gl!.uniform1f(snapshot[paramName].uniform, snapshot[paramName].value); } } } @@ -527,7 +527,7 @@ window.onload = () => { const filterSelector = FilterSelector(); imageSelector.addEventListener('imageSelected', function(e: CustomEvent) { filterSelector.updateImage$(e.detail.imageData); - }); + } as EventListener); filterSelectorEntry.appendChild(filterSelector); imageSelectorEntry.appendChild(imageSelector); @@ -556,4 +556,4 @@ window.onload = () => { gif = filterSelector.render$(`${fileName}.gif`); }; } -// TODO(#75): run typescript compiler on CI +// TODO(#75): run typescript compiler on CI \ No newline at end of file