diff --git a/.gitignore b/.gitignore index ac614744..a9ddbe3b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /telegram-bot/node_modules/ /telegram-bot/package-lock.json +/web/node_modules/ /web/documentation/ /web/fend-icon-128.png /web/pkg/ diff --git a/web/.prettierignore b/web/.prettierignore new file mode 100644 index 00000000..2d19fc76 --- /dev/null +++ b/web/.prettierignore @@ -0,0 +1 @@ +*.html diff --git a/web/build.sh b/web/build.sh index 0cf57f30..b5f6ff65 100755 --- a/web/build.sh +++ b/web/build.sh @@ -2,6 +2,9 @@ set -euo pipefail cd "$(dirname "$0")" +npm ci +npm run check + (cd ../wasm && wasm-pack build --target no-modules --out-dir ../web/pkg) convert -resize "128x128" ../icon/icon.svg fend-icon-128.png diff --git a/web/index.html b/web/index.html index b8bbfcb5..114edce4 100644 --- a/web/index.html +++ b/web/index.html @@ -10,17 +10,17 @@
-

- fend is an arbitrary-precision unit-aware calculator. -

-
-
- -
-

-
-
-

examples: +

+ fend is an arbitrary-precision unit-aware calculator. +

+
+
+ +
+

+
+
+

examples: > 5'10" to cm 177.8 cm @@ -38,7 +38,7 @@

> 1 lightyear to parsecs approx. 0.3066013937 parsecs give it a go:

-

+
diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 00000000..f128ab2c --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,31 @@ +{ + "name": "fend-web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fend-web", + "version": "0.0.0", + "license": "GPL-3.0-or-later", + "devDependencies": { + "prettier": "^3.2.5" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 00000000..43a0c683 --- /dev/null +++ b/web/package.json @@ -0,0 +1,29 @@ +{ + "name": "fend-web", + "private": true, + "version": "0.0.0", + "description": "", + "main": "widget.js", + "scripts": { + "format": "prettier --write .", + "check": "prettier --check ." + }, + "repository": { + "type": "git", + "url": "git+https://github.com/printfn/fend.git" + }, + "author": "printfn", + "license": "GPL-3.0-or-later", + "bugs": { + "url": "https://github.com/printfn/fend/issues" + }, + "homepage": "https://github.com/printfn/fend#readme", + "devDependencies": { + "prettier": "^3.2.5" + }, + "prettier": { + "useTabs": true, + "singleQuote": true, + "arrowParens": "avoid" + } +} diff --git a/web/widget.css b/web/widget.css index b9465b8a..f1d14b2b 100644 --- a/web/widget.css +++ b/web/widget.css @@ -1,95 +1,95 @@ * { - background: transparent; - color: inherit; - border: none; - font-weight: inherit; - font-size: inherit; - font-style: inherit; - margin: 0; - padding: 0; + background: transparent; + color: inherit; + border: none; + font-weight: inherit; + font-size: inherit; + font-style: inherit; + margin: 0; + padding: 0; } body { - background: hsl(30, 15%, 90%); - font: 16px/150% monospace; - color: hsl(30, 25%, 10%); - margin: 0; + background: hsl(30, 15%, 90%); + font: 16px/150% monospace; + color: hsl(30, 25%, 10%); + margin: 0; } a { - color: hsl(90, 70%, 30%); + color: hsl(90, 70%, 30%); } b { - color: hsl(30, 25%, 40%); + color: hsl(30, 25%, 40%); } @media (prefers-color-scheme: dark) { - a { - color: hsl(90, 70%, 70%); - } + a { + color: hsl(90, 70%, 70%); + } - b { - color: hsl(30, 25%, 70%); - } + b { + color: hsl(30, 25%, 70%); + } - body { - background: hsl(30, 25%, 10%); - color: hsl(30, 15%, 90%); - } + body { + background: hsl(30, 25%, 10%); + color: hsl(30, 15%, 90%); + } } main { - display: grid; - grid-template-columns: auto auto; - grid-template-rows: auto auto auto; - max-width: 60ch; - padding: 3ch; - margin: auto; + display: grid; + grid-template-columns: auto auto; + grid-template-rows: auto auto auto; + max-width: 60ch; + padding: 3ch; + margin: auto; } main > * { - grid-column: 1/3; + grid-column: 1/3; } #output { - grid-row: 2; + grid-row: 2; } #output p { - white-space: pre-wrap; + white-space: pre-wrap; } #input { - display: grid; - grid-template-columns: 2ch 1fr; - grid-template-rows: auto auto; - grid-row: 3; + display: grid; + grid-template-columns: 2ch 1fr; + grid-template-rows: auto auto; + grid-row: 3; } #input p { - grid-column: 1/3; - grid-row: 2; + grid-column: 1/3; + grid-row: 2; } #input:before { - content: ">"; + content: '>'; } #input #text { - display: grid; - grid-column: 2; - grid-row: 1; + display: grid; + grid-column: 2; + grid-row: 1; } #input #text textarea { - line-height: inherit; - font-family: inherit; - outline: none; - overflow: hidden; - resize: none; + line-height: inherit; + font-family: inherit; + outline: none; + overflow: hidden; + resize: none; } #input #text:after { - content: attr(data-replicated-value) " "; - visibility: hidden; + content: attr(data-replicated-value) ' '; + visibility: hidden; } #input #text textarea, #input #text:after { - grid-area: 1 / 1 / 2 / 2; - white-space: pre-wrap; -} \ No newline at end of file + grid-area: 1 / 1 / 2 / 2; + white-space: pre-wrap; +} diff --git a/web/widget.html b/web/widget.html index 74c21d89..ed4c0c0d 100644 --- a/web/widget.html +++ b/web/widget.html @@ -9,13 +9,13 @@
-
-
- -
-

-
-
+
+
+ +
+

+
+
diff --git a/web/widget.js b/web/widget.js index 4417616e..f504b708 100644 --- a/web/widget.js +++ b/web/widget.js @@ -1,236 +1,215 @@ const { - initialise, - initialiseWithHandlers, - evaluateFendWithTimeout, - evaluateFendWithVariablesJson, + initialise, + initialiseWithHandlers, + evaluateFendWithTimeout, + evaluateFendWithVariablesJson, } = wasm_bindgen; const EVALUATE_KEY = 13; const NAVIGATE_UP_KEY = 38; const NAVIGATE_DOWN_KEY = 40; -let output = document.getElementById("output"); -let inputText = document.getElementById("input-text"); -let inputHint = document.getElementById("input-hint"); +let output = document.getElementById('output'); +let inputText = document.getElementById('input-text'); +let inputHint = document.getElementById('input-hint'); let wasmInitialised = false; let history = []; -let variables = ""; +let variables = ''; let navigation = 0; async function evaluate(event) { - // allow multiple lines to be entered if shift, ctrl - // or meta is held, otherwise evaluate the expression - if ( - !(event.keyCode == EVALUATE_KEY && !event.shiftKey && !event.ctrlKey && - !event.metaKey) - ) { - return; - } + // allow multiple lines to be entered if shift, ctrl + // or meta is held, otherwise evaluate the expression + if ( + !( + event.keyCode == EVALUATE_KEY && + !event.shiftKey && + !event.ctrlKey && + !event.metaKey + ) + ) { + return; + } - event.preventDefault(); + event.preventDefault(); - if (inputText.value == "clear") { - output.innerHTML = ""; - inputText.value = ""; - inputHint.innerText = ""; - return; - } + if (inputText.value == 'clear') { + output.innerHTML = ''; + inputText.value = ''; + inputHint.innerText = ''; + return; + } - let request = document.createElement("p"); - let result = document.createElement("p"); + let request = document.createElement('p'); + let result = document.createElement('p'); - request.innerText = "> " + inputText.value; + request.innerText = '> ' + inputText.value; - if (isInputFilled()) { - history.push(inputText.value); - } + if (isInputFilled()) { + history.push(inputText.value); + } - navigateEnd(); + navigateEnd(); - const fendResult = JSON.parse( - evaluateFendWithVariablesJson(inputText.value, 500, variables), - ); + const fendResult = JSON.parse( + evaluateFendWithVariablesJson(inputText.value, 500, variables), + ); - inputText.value = ""; - inputHint.innerText = ""; + inputText.value = ''; + inputHint.innerText = ''; - console.log(fendResult); + console.log(fendResult); - result.innerText = fendResult.ok ? fendResult.result : fendResult.message; - if (fendResult.ok && fendResult.variables.length > 0) { - variables = fendResult.variables; - } + result.innerText = fendResult.ok ? fendResult.result : fendResult.message; + if (fendResult.ok && fendResult.variables.length > 0) { + variables = fendResult.variables; + } - output.appendChild(request); - output.appendChild(result); + output.appendChild(request); + output.appendChild(result); - inputHint.scrollIntoView(); + inputHint.scrollIntoView(); } function navigate(event) { - if (![NAVIGATE_UP_KEY, NAVIGATE_DOWN_KEY].includes(event.keyCode)) { - return; - } - if (navigation > 0) { - if (NAVIGATE_UP_KEY == event.keyCode) { - event.preventDefault(); - - navigateBackwards(); - } else if (NAVIGATE_DOWN_KEY == event.keyCode) { - event.preventDefault(); - - navigateForwards(); - } - } else if ( - !isInputFilled() && history.length > 0 && NAVIGATE_UP_KEY == event.keyCode - ) { - event.preventDefault(); - - navigateBegin(); - } - - if (navigation > 0) { - navigateSet(); - } - - updateReplicatedText(); - updateHint(); + if (![NAVIGATE_UP_KEY, NAVIGATE_DOWN_KEY].includes(event.keyCode)) { + return; + } + if (navigation > 0) { + if (NAVIGATE_UP_KEY == event.keyCode) { + event.preventDefault(); + + navigateBackwards(); + } else if (NAVIGATE_DOWN_KEY == event.keyCode) { + event.preventDefault(); + + navigateForwards(); + } + } else if ( + !isInputFilled() && + history.length > 0 && + NAVIGATE_UP_KEY == event.keyCode + ) { + event.preventDefault(); + + navigateBegin(); + } + + if (navigation > 0) { + navigateSet(); + } + + updateReplicatedText(); + updateHint(); } function navigateBackwards() { - navigation += 1; + navigation += 1; - if (navigation > history.length) { - navigation = history.length; - } + if (navigation > history.length) { + navigation = history.length; + } } function navigateForwards() { - navigation -= 1; + navigation -= 1; - if (navigation < 1) { - navigateEnd(); - navigateClear(); - } + if (navigation < 1) { + navigateEnd(); + navigateClear(); + } } function navigateBegin() { - navigation = 1; + navigation = 1; } function navigateEnd() { - navigation = 0; + navigation = 0; } function navigateSet() { - inputText.value = history[history.length - navigation]; + inputText.value = history[history.length - navigation]; } function navigateClear() { - inputText.value = ""; + inputText.value = ''; } function focus() { - // allow the user to select text for copying and - // pasting, but if text is deselected (collapsed) - // refocus the input field - if ( - document.activeElement != inputText && document.getSelection().isCollapsed - ) { - inputText.focus(); - } + // allow the user to select text for copying and + // pasting, but if text is deselected (collapsed) + // refocus the input field + if ( + document.activeElement != inputText && + document.getSelection().isCollapsed + ) { + inputText.focus(); + } } async function update() { - updateReplicatedText(); - navigateEnd(); - updateHint(); + updateReplicatedText(); + navigateEnd(); + updateHint(); } function updateReplicatedText() { - inputText.parentNode.dataset.replicatedValue = inputText.value; + inputText.parentNode.dataset.replicatedValue = inputText.value; } function updateHint() { - const result = JSON.parse( - evaluateFendWithVariablesJson(inputText.value, 100, variables), - ); - - if (result.ok) { - inputHint.innerText = result.result; - } else { - inputHint.innerText = ""; - } + const result = JSON.parse( + evaluateFendWithVariablesJson(inputText.value, 100, variables), + ); + + if (result.ok) { + inputHint.innerText = result.result; + } else { + inputHint.innerText = ''; + } } function isInputFilled() { - return inputText.value.length > 0; + return inputText.value.length > 0; } async function getExchangeRates() { - const map = new Map(); - - try { - const res = await fetch( - `https://corsproxy.io/?${ - encodeURIComponent( - "https://treasury.un.org/operationalrates/xsql2XML.php", - ) - }`, - ); - const xml = await res.text(); - const dom = new DOMParser().parseFromString(xml, "text/xml"); - - dom.querySelectorAll("UN_OPERATIONAL_RATES").forEach((node) => { - const currency = node.querySelector("f_curr_code").textContent; - const rate = parseFloat(node.querySelector("rate").textContent); - - if (!Number.isNaN(rate) && Number.isFinite(rate)) { - map.set(currency, rate); - } - }); - } catch (_) {} - - return map; -} + const map = new Map(); -async function getExchangeRates() { - const map = new Map(); - - try { - const res = await fetch( - `https://corsproxy.io/?${encodeURIComponent( - "https://treasury.un.org/operationalrates/xsql2XML.php", - ) - }`, - ); - const xml = await res.text(); - const dom = new DOMParser().parseFromString(xml, "text/xml"); - - dom.querySelectorAll("UN_OPERATIONAL_RATES").forEach((node) => { - const currency = node.querySelector("f_curr_code").textContent; - const rate = parseFloat(node.querySelector("rate").textContent); - - if (!Number.isNaN(rate) && Number.isFinite(rate)) { - map.set(currency, rate); - } - }); - } catch (_) { } - - return map; + try { + const res = await fetch( + `https://corsproxy.io/?${encodeURIComponent( + 'https://treasury.un.org/operationalrates/xsql2XML.php', + )}`, + ); + const xml = await res.text(); + const dom = new DOMParser().parseFromString(xml, 'text/xml'); + + dom.querySelectorAll('UN_OPERATIONAL_RATES').forEach(node => { + const currency = node.querySelector('f_curr_code').textContent; + const rate = parseFloat(node.querySelector('rate').textContent); + + if (!Number.isNaN(rate) && Number.isFinite(rate)) { + map.set(currency, rate); + } + }); + } catch (_) {} + + return map; } async function load() { - await wasm_bindgen("./pkg/fend_wasm_bg.wasm"); - initialiseWithHandlers(await getExchangeRates()); + await wasm_bindgen('./pkg/fend_wasm_bg.wasm'); + initialiseWithHandlers(await getExchangeRates()); - evaluateFendWithTimeout("1 + 2", 500); - wasmInitialised = true; + evaluateFendWithTimeout('1 + 2', 500); + wasmInitialised = true; - inputText.addEventListener("input", update); - inputText.addEventListener("keypress", evaluate); - inputText.addEventListener("keydown", navigate); - document.addEventListener("click", focus); + inputText.addEventListener('input', update); + inputText.addEventListener('keypress', evaluate); + inputText.addEventListener('keydown', navigate); + document.addEventListener('click', focus); } window.onload = load;