diff --git a/examples/calculator/.gitignore b/examples/calculator/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/examples/calculator/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/examples/calculator/.npmrc b/examples/calculator/.npmrc
new file mode 100644
index 00000000..6b5f38e8
--- /dev/null
+++ b/examples/calculator/.npmrc
@@ -0,0 +1,2 @@
+save-exact = true
+package-lock = false
diff --git a/examples/calculator/README.md b/examples/calculator/README.md
new file mode 100644
index 00000000..0e63872e
--- /dev/null
+++ b/examples/calculator/README.md
@@ -0,0 +1,23 @@
+# Frontend Mentor Calculator
+
+Here is the implementation in [Bau.js](https://github.com/grucloud/bau) of the [Frontend Mentor Calculator code challenge](https://www.frontendmentor.io/challenges/calculator-app-9lteq5N29)
+
+## Workflow
+
+Install the dependencies:
+
+```sh
+npm install
+```
+
+Start a development server:
+
+```sh
+npm run dev
+```
+
+Build a production version:
+
+```sh
+npm run build
+```
diff --git a/examples/calculator/index.html b/examples/calculator/index.html
new file mode 100644
index 00000000..3728b1c2
--- /dev/null
+++ b/examples/calculator/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ Calculator | FrontendMentor
+
+
+
+
+
+
diff --git a/examples/calculator/package.json b/examples/calculator/package.json
new file mode 100644
index 00000000..c3c67fb1
--- /dev/null
+++ b/examples/calculator/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "frontendmentor-mortgage-repayment-calculator",
+ "private": true,
+ "version": "0.85.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "test": "vitest"
+ },
+ "devDependencies": {
+ "typescript": "^5.0.2",
+ "vite": "^5.2.11"
+ },
+ "dependencies": {
+ "@grucloud/bau": "^0.85.0",
+ "@grucloud/bau-css": "^0.85.0",
+ "@grucloud/bau-ui": "^0.85.0",
+ "bignumber.js": "9.1.2",
+ "vitest": "2.1.4"
+ }
+}
diff --git a/examples/calculator/public/assets/images/favicon-32x32.png b/examples/calculator/public/assets/images/favicon-32x32.png
new file mode 100644
index 00000000..1e2df7f0
Binary files /dev/null and b/examples/calculator/public/assets/images/favicon-32x32.png differ
diff --git a/examples/calculator/src/calculator.ts b/examples/calculator/src/calculator.ts
new file mode 100644
index 00000000..f2c7a6e4
--- /dev/null
+++ b/examples/calculator/src/calculator.ts
@@ -0,0 +1,258 @@
+import { type Context } from "@grucloud/bau-ui/context";
+import themeSwitcher from "./themeSwitcher";
+import Parser, { Token } from "./parser";
+import { compute, buildRPN } from "./shuntingYard";
+import BN from "bignumber.js";
+
+const locale = "US";
+const symbols = [
+ ["7"],
+ ["8"],
+ ["9"],
+ ["DEL", "del"],
+ ["4"],
+ ["5"],
+ ["6"],
+ ["+", "add"],
+ ["1"],
+ ["2"],
+ ["3"],
+ ["-", "minus"],
+ [".", "dot"],
+ ["0"],
+ ["/", "divide"],
+ ["*", "multiply"],
+ ["RESET", "reset"],
+ ["=", "equal"],
+];
+
+const formatNumber = (num: string) => {
+ const formatted = new Intl.NumberFormat(locale, {
+ maximumFractionDigits: 50,
+ }).format(Number(num));
+ return formatted.length < num.length ? num : formatted;
+};
+
+export default function (context: Context) {
+ const { bau, css, window } = context;
+ const { h1, article, header, section, button, div } = bau.tags;
+
+ const tokensState = bau.state([]);
+ const rpn = bau.derive(() => buildRPN(tokensState.val));
+ // const rpnString = bau.derive(() =>
+ // rpn.val.map(({ value }) => value).join(" ")
+ // );
+ const result = bau.derive(() => {
+ const resultValue = compute(rpn.val).resultValue;
+ if (resultValue) {
+ if (!resultValue.isNaN()) {
+ return formatNumber(resultValue.toString());
+ }
+ }
+ });
+
+ const operandCurrent = bau.derive(() =>
+ tokensState.val.reduce(
+ (acc, { value }) =>
+ BN(value).isNaN() ? acc.concat(value) : acc.concat(formatNumber(value)),
+ ""
+ )
+ );
+
+ const className = css`
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 0.3rem;
+ padding-inline: 1.5rem;
+ padding-block: 1rem;
+ color: var(--color-text);
+ min-height: 100vh;
+ > header {
+ display: flex;
+ justify-content: space-between;
+ gap: 1rem;
+ }
+ > section {
+ background-color: var(--secondary-background-color);
+ padding: 1rem;
+ border-radius: 0.5rem;
+ }
+ .keypad {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(4, 1fr);
+ button {
+ font-size: 1.8rem;
+ font-weight: 700;
+ border: none;
+ padding-inline: 1rem;
+ padding-block: 0.7rem;
+ cursor: pointer;
+ border-radius: 0.5rem;
+ background: var(--buttons-background-color);
+ color: var(--buttons-color-text);
+ box-shadow: inset 0px -4px 0px var(--buttons-box-shadow);
+ transition: all 0.1s ease-out;
+ &:hover {
+ background: var(--buttons-background-color-active);
+ }
+ &:active {
+ transform: translateY(1px);
+ box-shadow: inset 0px -1px 0px var(--buttons-box-shadow);
+ }
+ }
+ .key-del,
+ .key-reset {
+ background-color: var(--buttons-secondary-background-color);
+ box-shadow: inset 0px -4px 0px var(--buttons-secondary-box-shadow);
+ color: #ffffff;
+ font-size: 1.2rem;
+ &:hover {
+ background: var(--buttons-secondary-background-color-active);
+ }
+ }
+ .key-reset {
+ grid-column: 1 / span 2;
+ }
+ .key-equal {
+ grid-column: 3 / span 2;
+ background-color: var(--ternary-background-color);
+ &:hover {
+ background: var(--ternary-background-color-active);
+ }
+ box-shadow: inset 0px -4px 0px var(--buttons-ternary-box-shadow);
+ color: #ffffff;
+ }
+ }
+ .display {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ justify-content: flex-end;
+ .operand-previous {
+ font-size: 1.2rem;
+ height: 2rem;
+ color: var(--color-text-secondary);
+ }
+ .operand-current {
+ font-size: 1.7rem;
+ height: 2rem;
+ }
+ }
+ `;
+
+ const parser = Parser();
+
+ const equalSign = () => {
+ if (result.val != undefined) {
+ parser.reset();
+ tokensState.val = parser.parseFormula(result.val);
+ }
+ };
+
+ const onclickSymbol = (symbol: any) => () => {
+ if (symbol[0] == "=") {
+ equalSign();
+ } else {
+ tokensState.val = parser.evKey(symbol[0]);
+ }
+ };
+
+ const onkeydown = (event: any) => {
+ if (event.key == "Backspace") {
+ tokensState.val = parser.evKey("DEL");
+ } else if (event.key == "Escape") {
+ parser.reset();
+ tokensState.val = [];
+ } else if (event.key == "=") {
+ equalSign();
+ } else {
+ tokensState.val = parser.evKey(event.key);
+ }
+ };
+
+ const ThemeSwitcher = themeSwitcher(context);
+
+ const getKeyName = (symbol: string[]) => symbol[1] ?? symbol[0];
+
+ function textWidth(str: string, fontSize: Number) {
+ const span = document.createElement("span");
+ span.style.position = "fixed";
+ span.style.visibility = "hidden";
+ span.style.fontSize = `${fontSize}px`;
+ span.innerText = str;
+ document.body.appendChild(span);
+ const width = Math.round(span.getBoundingClientRect().width);
+ span.remove();
+ return width;
+ }
+
+ const doFontSize = (state: any, fsStart = 20) => {
+ let fs = fsStart;
+ if (!state.val) {
+ return fs;
+ }
+ const el = document.getElementsByClassName("display")[0];
+ if (!el) {
+ return fs;
+ }
+ const width = el.getBoundingClientRect().width;
+ while (fs > 8) {
+ if (textWidth(state.val, fs) + 50 > width) {
+ fs--;
+ } else {
+ break;
+ }
+ }
+ return fs;
+ };
+
+ const fsOperandCurrent = bau.derive(() => doFontSize(operandCurrent, 22));
+ const fsResult = bau.derive(() => doFontSize(result, 18));
+
+ return () => {
+ return article(
+ {
+ class: className,
+ bauMounted: () => {
+ window.document.body.addEventListener("keydown", onkeydown);
+ },
+ bauUnmounted: () => {
+ window.document.removeEventListener("keydown", onkeydown);
+ },
+ },
+ header(h1("Calc"), ThemeSwitcher()),
+ section(
+ { class: "display" },
+ div(
+ {
+ style: () => `font-size: ${fsResult.val}px`,
+ class: "operand-previous",
+ },
+ result
+ ),
+ div(
+ {
+ style: () => `font-size: ${fsOperandCurrent.val}px`,
+ class: "operand-current",
+ },
+ operandCurrent
+ )
+ ),
+ section(
+ { class: "keypad" },
+ symbols.map((symbol) =>
+ button(
+ {
+ type: "button",
+ class: `key-${getKeyName(symbol)}`,
+ onclick: onclickSymbol(symbol),
+ },
+ symbol[0]
+ )
+ )
+ )
+ );
+ };
+}
diff --git a/examples/calculator/src/main.ts b/examples/calculator/src/main.ts
new file mode 100644
index 00000000..f8006eb7
--- /dev/null
+++ b/examples/calculator/src/main.ts
@@ -0,0 +1,20 @@
+import { createContext, type Context } from "@grucloud/bau-ui/context";
+import calculator from "./calculator";
+
+import "./style.css";
+
+const context = createContext();
+
+const app = (context: Context) => {
+ const { bau } = context;
+ const { main } = bau.tags;
+
+ const Calculator = calculator(context);
+
+ return function () {
+ return main(Calculator());
+ };
+};
+
+const App = app(context);
+document.getElementById("app")?.replaceChildren(App());
diff --git a/examples/calculator/src/parser.ts b/examples/calculator/src/parser.ts
new file mode 100644
index 00000000..c35738c5
--- /dev/null
+++ b/examples/calculator/src/parser.ts
@@ -0,0 +1,126 @@
+export const TOKEN_TYPE = { NUMERIC: "NUMERIC", OPERATOR: "OPERATOR" };
+
+const STATES = {
+ INIT: "INIT",
+ COLLECT_DIGIT: "COLLECT_DIGIT",
+ COLLECT_OPERATOR: "COLLECT_OPERATOR",
+};
+
+export type Token = {
+ value: string;
+ type: string;
+};
+
+const isDigit = (key: string) => {
+ const nkey = Number(key);
+ return nkey >= 0 && nkey <= 9 ? true : false;
+};
+
+const isDot = (key: string) => key == ".";
+const isMinus = (key: string) => key == "-";
+
+const isOperator = (key: string) => ["*", "/", "+", "-", "="].includes(key);
+const isDelete = (key: string) => key == "DEL";
+const isReset = (key: string) => key == "RESET";
+
+const Parser = () => {
+ let _formula = "";
+ const parseFormula = (formula: string) => {
+ _formula = formula;
+ let stateCurrent = STATES.INIT;
+ let stateNext = STATES.INIT;
+ const tokens: Token[] = [];
+ let tokenCurrent: string = "";
+
+ const onTokenNew = (token: Token) => tokens.unshift(token);
+
+ const onTokenUpdate = (token: Token) => {
+ if (tokens.length > 0) {
+ tokens[0] = token;
+ }
+ };
+
+ const tokenUpdate = (key: string) => {
+ tokenCurrent = tokenCurrent.concat(key);
+ };
+
+ const resetToken = () => {
+ tokenCurrent = "";
+ };
+
+ formula.split("").forEach((key) => {
+ stateNext = "";
+ switch (stateCurrent) {
+ case STATES.INIT: {
+ if (isDigit(key) || isDot(key) || isMinus(key)) {
+ stateNext = STATES.COLLECT_DIGIT;
+ } else if (isOperator(key)) {
+ stateNext = STATES.COLLECT_OPERATOR;
+ }
+ break;
+ }
+ case STATES.COLLECT_DIGIT: {
+ if (isDigit(key) || (isDot(key) && !tokenCurrent.includes("."))) {
+ tokenUpdate(key);
+ onTokenUpdate({ value: tokenCurrent, type: TOKEN_TYPE.NUMERIC });
+ } else if (isOperator(key)) {
+ stateNext = STATES.COLLECT_OPERATOR;
+ }
+ break;
+ }
+ case STATES.COLLECT_OPERATOR: {
+ if (isDigit(key) || isDot(key) || isMinus(key)) {
+ stateNext = STATES.COLLECT_DIGIT;
+ }
+ //Ignore isOperator
+ break;
+ }
+ default: {
+ throw Error("Invalid State");
+ }
+ }
+ // On Entry
+ switch (stateNext) {
+ case STATES.INIT: {
+ resetToken();
+ break;
+ }
+ case STATES.COLLECT_DIGIT: {
+ tokenUpdate(key);
+ onTokenNew({ value: tokenCurrent, type: TOKEN_TYPE.NUMERIC });
+ break;
+ }
+ case STATES.COLLECT_OPERATOR: {
+ resetToken();
+ onTokenNew({ value: key, type: TOKEN_TYPE.OPERATOR });
+ break;
+ }
+ }
+ if (stateNext) {
+ stateCurrent = stateNext;
+ }
+ });
+ return tokens.reverse();
+ };
+
+ const reset = () => {
+ _formula = "";
+ };
+
+ return {
+ reset,
+ parseFormula,
+ evKey: (key: string) => {
+ if (isReset(key)) {
+ reset();
+ } else if (isDelete(key)) {
+ _formula = _formula.slice(0, -1);
+ } else if (isDigit(key) || isDot(key) || isOperator(key)) {
+ _formula = _formula.concat(key);
+ }
+ return parseFormula(_formula);
+ },
+ };
+};
+
+export default Parser;
diff --git a/examples/calculator/src/shuntingYard.ts b/examples/calculator/src/shuntingYard.ts
new file mode 100644
index 00000000..71fbe0f0
--- /dev/null
+++ b/examples/calculator/src/shuntingYard.ts
@@ -0,0 +1,93 @@
+import { TOKEN_TYPE, Token } from "./parser";
+import BN from "bignumber.js";
+
+const OperatorMap: Record = {
+ "/": 4,
+ "*": 3,
+ "+": 2,
+ "-": 2,
+};
+
+const getPrecedence = (token: Token) => OperatorMap[token.value];
+
+// Returns the Reversed Polished Notation
+export const buildRPN = (tokens: Token[]) => {
+ const output: Token[] = [];
+ const stack: Token[] = [];
+
+ tokens.forEach((token) => {
+ if (token.type == TOKEN_TYPE.NUMERIC) {
+ output.push(token);
+ } else if (token.type == TOKEN_TYPE.OPERATOR) {
+ while (stack.length > 0) {
+ const itToken = stack[0];
+ if (getPrecedence(itToken) >= getPrecedence(token)) {
+ stack.shift();
+ output.push(itToken);
+ } else {
+ break;
+ }
+ }
+ stack.unshift(token);
+ }
+ });
+ while (stack.length > 0) {
+ const toMove = stack.shift();
+ if (toMove) {
+ output.push(toMove);
+ }
+ }
+
+ return output;
+};
+
+export const compute = (tokens: Token[]) => {
+ const results: BN[] = [];
+ let error;
+ tokens.every((token) => {
+ if (token.type == TOKEN_TYPE.NUMERIC) {
+ results.unshift(BN(token.value));
+ } else if (token.type == TOKEN_TYPE.OPERATOR) {
+ const op = token.value;
+ // Assume operators consumes 2 operands
+ const operand2 = results.shift();
+ if (!operand2) {
+ error = "missing operand";
+ return false;
+ }
+ const operand1 = results.shift();
+ if (!operand1) {
+ error = "missing operand";
+ return false;
+ }
+ let result;
+ switch (op) {
+ case "*": {
+ result = operand1.times(operand2);
+ break;
+ }
+ case "/": {
+ result = operand1.dividedBy(operand2);
+ break;
+ }
+ case "+": {
+ result = operand1.plus(operand2);
+ break;
+ }
+ case "-": {
+ result = operand1.minus(operand2);
+ break;
+ }
+ }
+ if (result) {
+ results.unshift(result);
+ }
+ }
+ return true;
+ });
+ let resultValue;
+ if (results.length == 1) {
+ resultValue = results[0];
+ }
+ return { results, error, resultValue };
+};
diff --git a/examples/calculator/src/style.css b/examples/calculator/src/style.css
new file mode 100644
index 00000000..bacc52d3
--- /dev/null
+++ b/examples/calculator/src/style.css
@@ -0,0 +1,72 @@
+@import url("https://fonts.googleapis.com/css2?family=Spartan:wght@700&display=swap");
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --primary-background-color: #3a4663;
+ --color-text: #fff;
+ --color-text-secondary: #9b9b9b;
+
+ --secondary-background-color: #242d44;
+ --ternary-background-color: #d03f2f;
+ --ternary-background-color-active: #f96b5b;
+ --display-background-color: #181f33;
+
+ --buttons-background-color: #eae3dc;
+ --buttons-background-color-active: #fffffe;
+ --buttons-secondary-background-color: #647198;
+ --buttons-secondary-background-color-active: #a2b2e1;
+ --buttons-secondary-box-shadow: #414e73;
+ --buttons-ternary-box-shadow: #93261a;
+ --buttons-color-text: #434a59;
+ --buttons-box-shadow: #b3a497;
+}
+
+[data-theme="second"] {
+ --primary-background-color: #e6e6e6;
+ --color-text: #36362c;
+ --secondary-background-color: #d2cdcd;
+ --ternary-background-color: #c85402;
+ --ternary-background-color-active: #ff8a38;
+ --display-background-color: #eeeeee;
+ --buttons-background-color: #e5e4e1;
+ --buttons-background-color-active: #ffffff;
+ --buttons-secondary-background-color: #378187;
+ --buttons-secondary-background-color-active: #62b5bc;
+ --buttons-secondary-box-shadow: #1b6066;
+ --buttons-ternary-box-shadow: #873901;
+ --buttons-color-text: var(--color-texts);
+ --buttons-box-shadow: #a79e91;
+}
+
+[data-theme="third"] {
+ --primary-background-color: #17062a;
+ --color-text: #ffe53d;
+ --secondary-background-color: #1e0936;
+ --ternary-background-color: #00ded0;
+ --ternary-background-color-active: #93fff8;
+ --display-background-color: #1e0936;
+ --buttons-background-color: #331c4d;
+ --buttons-background-color-active: #6c34ac;
+ --buttons-secondary-background-color: #56077c;
+ --buttons-secondary-background-color: #8631af;
+ --buttons-secondary-box-shadow: #be15f4;
+ --buttons-ternary-box-shadow: #6cf9f1;
+ --buttons-color-text: var(--color-texts);
+ --buttons-box-shadow: #881c9e;
+}
+
+body {
+ background-color: var(--primary-background-color);
+ font-family: "Spartan", sans-serif;
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+ @media (max-width: 500px) {
+ place-items: flex-start;
+ }
+}
diff --git a/examples/calculator/src/themeSwitcher.ts b/examples/calculator/src/themeSwitcher.ts
new file mode 100644
index 00000000..297d454f
--- /dev/null
+++ b/examples/calculator/src/themeSwitcher.ts
@@ -0,0 +1,102 @@
+import { type Context } from "@grucloud/bau-ui/context";
+
+const themes = ["first", "second", "third"];
+
+export default function (context: Context) {
+ const { bau, css, window } = context;
+ const { section, span, input, div, label } = bau.tags;
+
+ const className = css`
+ display: flex;
+ align-items: flex-end;
+ gap: 0.4rem;
+
+ > span {
+ font-size: 0.6rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1rem;
+ }
+
+ label {
+ font-size: 0.6rem;
+ cursor: pointer;
+ }
+
+ input {
+ cursor: pointer;
+ appearance: none;
+ width: 1.2rem;
+ height: 1.2rem;
+ }
+
+ .label-container {
+ display: flex;
+ justify-content: space-around;
+ }
+ .input-container {
+ display: flex;
+ position: relative;
+ margin-inline: 0.2rem;
+ background: var(--secondary-background-color);
+ border-radius: 1rem;
+ &::before {
+ position: absolute;
+ content: "";
+ left: 5%;
+ top: 50%;
+ transform: translateY(-50%);
+ height: 0.6rem;
+ width: 0.6rem;
+ border-radius: 50%;
+ background-color: var(--ternary-background-color);
+ transition: all 0.5s;
+ }
+ &:has(input[id="second"]:checked) {
+ &::before {
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+ }
+ &:has(input[id="third"]:checked) {
+ &::before {
+ left: 95%;
+ transform: translate(-100%, -50%);
+ }
+ }
+ }
+ `;
+
+ const onsubmitTheme = (event: any) => {
+ window.document.documentElement.setAttribute("data-theme", event.target.id);
+ };
+
+ return () =>
+ section(
+ {
+ class: className,
+ onclick: onsubmitTheme,
+ },
+ span("Theme"),
+ div(
+ div(
+ {
+ class: "label-container",
+ },
+ themes.map((id, index) => label({ htmlFor: id }, index + 1))
+ ),
+ div(
+ {
+ class: "input-container",
+ },
+ themes.map((id) =>
+ input({
+ type: "radio",
+ name: "themeRadio",
+ id,
+ oninput: onsubmitTheme,
+ })
+ )
+ )
+ )
+ );
+}
diff --git a/examples/calculator/src/vite-env.d.ts b/examples/calculator/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/examples/calculator/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/calculator/test/parser.test.ts b/examples/calculator/test/parser.test.ts
new file mode 100644
index 00000000..57eb64dd
--- /dev/null
+++ b/examples/calculator/test/parser.test.ts
@@ -0,0 +1,46 @@
+import { beforeEach, describe, it, assert } from "vitest";
+
+import Parser, { TOKEN_TYPE } from "../src/parser";
+
+describe("parser", async () => {
+ const parser = Parser();
+
+ beforeEach(() => {
+ parser.reset();
+ });
+
+ it("1+1=", () => {
+ "1+1=".split("").forEach((key) => parser.evKey(key));
+ const tokens = parser.evKey("1");
+ assert(tokens);
+ });
+ it("-1+1", () => {
+ const tokens = parser.parseFormula("-1+1");
+ console.log(JSON.stringify(tokens));
+ assert.equal(tokens.length, 3);
+ assert.equal(tokens[0].value, "-1");
+ assert.equal(tokens[0].type, TOKEN_TYPE.NUMERIC);
+ assert.equal(tokens[1].type, TOKEN_TYPE.OPERATOR);
+ });
+ it("1+2*3", () => {
+ const tokens = parser.parseFormula("1+2*3");
+ assert.equal(tokens.length, 5);
+ });
+ it("1.1+0.0", () => {
+ const tokens = parser.parseFormula("1.1+0.0");
+ assert.equal(tokens.length, 3);
+ assert.equal(tokens[0].value, "1.1");
+ assert.equal(tokens[0].type, TOKEN_TYPE.NUMERIC);
+ assert.equal(tokens[1].type, TOKEN_TYPE.OPERATOR);
+ });
+ it("1", () => {
+ const tokens = parser.parseFormula("1");
+ assert.equal(tokens.length, 1);
+ assert.equal(tokens[0].value, "1");
+ });
+ it("/", () => {
+ const tokens = parser.parseFormula("/");
+ assert.equal(tokens.length, 1);
+ assert.equal(tokens[0].value, "/");
+ });
+});
diff --git a/examples/calculator/test/shuntingYard.test.ts b/examples/calculator/test/shuntingYard.test.ts
new file mode 100644
index 00000000..d3f8e275
--- /dev/null
+++ b/examples/calculator/test/shuntingYard.test.ts
@@ -0,0 +1,65 @@
+import { beforeEach, describe, it, assert } from "vitest";
+
+import Parser from "../src/parser";
+import { buildRPN, compute } from "../src/shuntingYard";
+
+const rpnToString = (tokens) => tokens.map(({ value }) => value).join(" ");
+
+describe("shunting yard", async () => {
+ const parser = Parser();
+
+ beforeEach(() => {
+ parser.reset();
+ });
+
+ it("1+2", () => {
+ const rpn = buildRPN(parser.parseFormula("1+2"));
+ assert.equal(rpnToString(rpn), "1 2 +");
+ assert.equal(compute(rpn).resultValue.toString(), 3);
+ });
+ it("-1-2-3", () => {
+ const rpn = buildRPN(parser.parseFormula("-1-2-3"));
+ assert.equal(rpnToString(rpn), "-1 2 - 3 -");
+ assert.equal(compute(rpn).resultValue.toString(), "-6");
+ });
+ it("2*-1", () => {
+ const rpn = buildRPN(parser.parseFormula("2*-1"));
+ assert.equal(rpnToString(rpn), "2 -1 *");
+ assert.equal(compute(rpn).resultValue.toString(), "-2");
+ });
+ it("1+2+3", () => {
+ const rpn = buildRPN(parser.parseFormula("1+2+3"));
+ assert.equal(rpnToString(rpn), "1 2 + 3 +");
+ assert.equal(compute(rpn).resultValue.toString(), "6");
+ });
+ it("2-1+3", () => {
+ const rpn = buildRPN(parser.parseFormula("2-1+3"));
+ assert.equal(rpnToString(rpn), "2 1 - 3 +");
+ assert.equal(compute(rpn).resultValue.toString(), 4);
+ });
+ it("1+2*4", () => {
+ const rpn = buildRPN(parser.parseFormula("1+2*4"));
+ assert.equal(rpnToString(rpn), "1 2 4 * +");
+ assert.equal(compute(rpn).resultValue.toString(), "9");
+ });
+ it("4/2", () => {
+ const rpn = buildRPN(parser.parseFormula("4/2"));
+ assert.equal(rpnToString(rpn), "4 2 /");
+ assert.equal(compute(rpn).resultValue.toString(), "2");
+ });
+ it("4/2*2", () => {
+ const rpn = buildRPN(parser.parseFormula("4/2*2"));
+ assert.equal(rpnToString(rpn), "4 2 / 2 *");
+ assert.equal(compute(rpn).resultValue.toString(), "4");
+ });
+ it("minus", () => {
+ const rpn = buildRPN(parser.parseFormula("-"));
+ assert.equal(rpnToString(rpn), "-");
+ assert.equal(compute(rpn).resultValue.toString(), "NaN");
+ });
+ it("1*-.2", () => {
+ const rpn = buildRPN(parser.parseFormula("1*-.2"));
+ assert.equal(rpnToString(rpn), "1 -.2 *");
+ assert.equal(compute(rpn).resultValue.toString(), "-0.2");
+ });
+});
diff --git a/examples/calculator/tsconfig.json b/examples/calculator/tsconfig.json
new file mode 100644
index 00000000..75abdef2
--- /dev/null
+++ b/examples/calculator/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/examples/calculator/vite.config.js b/examples/calculator/vite.config.js
new file mode 100644
index 00000000..41713bec
--- /dev/null
+++ b/examples/calculator/vite.config.js
@@ -0,0 +1,9 @@
+import { defineConfig } from "vite";
+
+export default defineConfig(({ command, mode, ssrBuild }) => {
+ return {
+ server: {
+ open: true,
+ },
+ };
+});
diff --git a/examples/contact-form/package.json b/examples/contact-form/package.json
index c17282b6..069be014 100644
--- a/examples/contact-form/package.json
+++ b/examples/contact-form/package.json
@@ -1,5 +1,5 @@
{
- "name": "frontendmentor-mortgage-repayment-calculator",
+ "name": "frontendmentor-contact-form",
"private": true,
"version": "0.85.0",
"type": "module",