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;