diff --git a/.gitignore b/.gitignore
index d768e6a259955366716e99dfd9197bb79dc24209..f219171b4eaedbf2be28de4585d04ce3b7292b9f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,4 @@ venv
 
 .vscode/
 
-dsiUnits-js/node_modules/.bin/
-
 dsiUnits-js/node_modules/
diff --git a/.vscode/launch.json b/.vscode/launch.json
deleted file mode 100644
index ae998543b0f8288275b68f8821088479f721d60b..0000000000000000000000000000000000000000
--- a/.vscode/launch.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-    // Use IntelliSense to learn about possible attributes.
-    // Hover to view descriptions of existing attributes.
-    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
-    "version": "0.2.0",
-    "configurations": [
-        {
-            "name": "Pytest: Debugger",
-            "type": "debugpy",
-            "request": "launch",
-            "program": "${workspaceFolder}/venv/bin/pytest",
-            "console": "integratedTerminal",
-            "justMyCode": false,
-            "args": ["--no-cov"],
-        }
-    ]
-}
\ No newline at end of file
diff --git a/README.md b/README.md
index 3fadffb9f6fa73b2ad378fe400d1597a743e86c2..f065202a9268f4ec1fb0f5afc442fb60a7ba68d0 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,11 @@
 This library converts D-SI unit strings to Latex.
 And is able to perform math operations *, / and power with the D-SI units as well as checken weather teh can be converted into each other with scalar multiplication
 
+
+## Javascript version
+
+The folder [dsiUnits-js](/dsiUnits-js) contains an Javascript version of this libary that can be used for a D-SI-Unit-Input Element with autosugestion. And a dsiUnit CLass that supports parsing and rendering (HTML converion) of the dsi Units. 
+
 ## Installation
 
 ```bash
diff --git a/dsiUnits-js/README.md b/dsiUnits-js/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..78221f1e68e6ff7376ce1d2fdf6ce69a9b064306
--- /dev/null
+++ b/dsiUnits-js/README.md
@@ -0,0 +1,115 @@
+# DSI Units JS Library
+
+**DSI Units JS Library** is a JavaScript library for parsing, rendering, and interactively editing D‑SI unit strings. 
+This library is a mainly LMM-generated version of the corresponding [dsiunits](https://pypi.org/project/dsiunits/) Python package. (Repository: [digitaldynamicmeasurement/dsiUnits](https://gitlab1.ptb.de/digitaldynamicmeasurement/dsiUnits)).
+
+The library includes two main components:
+
+- **DSIUnit** – a class for parsing and rendering D‑SI unit strings.
+- **DSIUnitInput** – a custom web component (`<dsi-unit-input>`) that provides an auto‑complete interactive input field for D‑SI units.
+
+## License
+
+This project is licensed under the [LGPL-2.1](../LICENSE) License.
+## Installation
+
+You will be able to install the library via npm in near future:
+
+```bash
+npm install dsiunits-js
+```
+
+Or clone the repository and bundle the code with your preferred bundler (Webpack, Rollup, etc.).
+
+## Usage
+
+### Importing the Library
+
+For ES modules, import the classes as follows:
+
+```js
+import { DSIUnit } from "dsiunits-js/src/dsiUnit.js";
+import "dsiunits-js/src/dsiUnitInput.js"; // This registers the <dsi-unit-input> custom element.
+```
+
+### Using the DSIUnit Class
+
+The `DSIUnit` class is used to parse and render a D‑SI unit string.
+
+#### Constructor
+
+```js
+const unit = new DSIUnit("\\metre\\tothe{2}");
+```
+
+- **Parameters:**
+  - `dsiString` *(string)*: The raw D‑SI unit string (e.g., `"\metre\tothe{2}"`).
+
+#### Properties
+
+- `dsiString` – The original D‑SI string.
+- `tree` – The parsed representation of the D‑SI unit.
+- `warnings` – An array of warnings encountered during parsing.
+- `nonDsiUnit` – A boolean indicating whether the string was marked as a non‑DSI unit.
+- `valid` – A boolean indicating whether the parsed unit is valid.
+- `scaleFactor` – The overall scale factor (default: 1.0).
+
+#### Methods
+
+- **toHTML(options)**
+  
+  Renders the unit as an HTML string.
+  
+  **Parameters:**
+  - `options` *(object, optional)*:
+    - `wrapper` *(string)*: String to wrap the rendered output.
+    - `prefix` *(string)*: String to prepend.
+    - `suffix` *(string)*: String to append.
+    - `oneLine` *(boolean)*: If `true`, renders a compact, single‑line version (fractions with “/” and roots in inline format).
+
+  **Example:**
+
+  ```js
+  console.log(unit.toHTML({ oneLine: true }));
+  ```
+
+- **toString()**
+
+  Returns the canonical D‑SI string representation.
+
+  ```js
+  console.log(unit.toString());
+  ```
+
+### Using the DSIUnitInput Web Component
+
+The `DSIUnitInput` custom element provides an interactive auto‑complete input for D‑SI unit strings.
+
+#### HTML Usage
+
+```html
+<dsi-unit-input suggestions-list="kilo,mega,milli,metre,second,per,tothe"></dsi-unit-input>
+```
+
+#### Features
+
+- **Auto‑Completion:**  
+  As you type after a backslash (`\\`), suggestions are provided based on context:
+  - If a prefix is accepted (e.g., `\milli`), only allowed units are suggested.
+  - If a unit is accepted, all allowed tokens (including `\per` and `\tothe`) are suggested.
+  - Arrow keys navigate suggestions; Tab, Enter, or Right Arrow accept a suggestion.
+
+- **Focus/Blur Behavior:**  
+  When the field loses focus, the rendered unit (using `DSIUnit.toHTML({ oneLine: true })`) is shown. Clicking the rendered output restores the raw input for editing.
+
+- **Customization:**  
+  Override the allowed tokens by setting the `suggestions-list` attribute with a comma‑separated list.
+
+#### Properties
+
+- **value**  
+  Returns the current raw D‑SI unit string.
+
+- **live**  
+  A boolean property (default: `true`) to enable/disable live updating.
+
diff --git a/dsiUnits-js/babel.config.json b/dsiUnits-js/babel.config.json
new file mode 100644
index 0000000000000000000000000000000000000000..8d73c432f25f98a6157467923d9b72bbac7a7c98
--- /dev/null
+++ b/dsiUnits-js/babel.config.json
@@ -0,0 +1,5 @@
+{
+  "presets": [
+    ["@babel/preset-env", { "targets": { "node": "current" } }]
+  ]
+}
diff --git a/dsiUnits-js/jest.config.mjs b/dsiUnits-js/jest.config.mjs
new file mode 100644
index 0000000000000000000000000000000000000000..24908d5c66bf3596c7aaf26620462945fa4d4422
--- /dev/null
+++ b/dsiUnits-js/jest.config.mjs
@@ -0,0 +1,4 @@
+export default {
+    testEnvironment: "jsdom",
+    testEnvironmentOptions: {}
+};
\ No newline at end of file
diff --git a/dsiUnits-js/package.json b/dsiUnits-js/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..30a112324ae5231f05dba1b92ac16b974a81d959
--- /dev/null
+++ b/dsiUnits-js/package.json
@@ -0,0 +1,21 @@
+{
+  "name": "dsiunits-js",
+  "version": "0.9.1",
+  "type": "module",
+  "main": "dist/index.cjs.js",
+  "module": "dist/index.esm.js",
+  "scripts": {
+    "build": "rollup -c",
+    "test": "jest"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.0.0",
+    "@babel/preset-env": "^7.0.0",
+    "@rollup/plugin-commonjs": "^21.0.0",
+    "@rollup/plugin-node-resolve": "^15.0.0",
+    "babel-jest": "^27.0.0",
+    "jest": "^28.1.3",
+    "jest-environment-jsdom": "^28.1.3",
+    "rollup": "^2.79.2"
+  }
+}
diff --git a/dsiUnits-js/rollup.config.js b/dsiUnits-js/rollup.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..62f2bd60f433f0a5f9b5e5566b83e53504c374ec
--- /dev/null
+++ b/dsiUnits-js/rollup.config.js
@@ -0,0 +1,22 @@
+import resolve from '@rollup/plugin-node-resolve';
+import commonjs from '@rollup/plugin-commonjs';
+
+export default {
+    input: 'src/index.js', // Entry point (see below)
+    output: [
+        {
+            file: 'dist/index.esm.js',
+            format: 'esm',
+            sourcemap: true
+        },
+        {
+            file: 'dist/index.cjs.js',
+            format: 'cjs',
+            sourcemap: true
+        }
+    ],
+    plugins: [
+        resolve(),
+        commonjs()
+    ]
+};
diff --git a/dsiUnits-js/src/dsiParser.js b/dsiUnits-js/src/dsiParser.js
new file mode 100644
index 0000000000000000000000000000000000000000..ca64da1469b691595f00c42496a9ef9b9f63a5af
--- /dev/null
+++ b/dsiUnits-js/src/dsiParser.js
@@ -0,0 +1,128 @@
+// dsiParser.js
+import { DSIUnitNode } from './dsiUnitNode.js';
+import { dsiPrefixesHTML, dsiUnitsHTML, dsiKeyWords } from './unitStrings.js';
+
+class DSIParser {
+  constructor() {
+    // Default options
+    this.createPerByDivision = true;
+    this.maxDenominator = 10000;
+    // Default wrappers for HTML output are not needed since we output directly HTML
+  }
+
+  // Singleton pattern
+  static getInstance() {
+    if (!DSIParser.instance) {
+      DSIParser.instance = new DSIParser();
+    }
+    return DSIParser.instance;
+  }
+
+  parse(dsiString) {
+    const warnings = [];
+
+    // Check for non-DSI marker
+    if (dsiString.length > 0 && dsiString[0] === '|') {
+      warnings.push("Parsing a correctly marked non D-SI unit!");
+      return {
+        original: dsiString,
+        tree: [[new DSIUnitNode('', dsiString.slice(1), '', false)]],
+        warnings,
+        nonDsiUnit: true
+      };
+    }
+
+    // Replace double backslashes with single backslash
+    while (dsiString.includes('\\\\')) {
+      warnings.push(`Double backslash found in string, treating as one backslash: «${dsiString}»`);
+      dsiString = dsiString.replace(/\\\\/g, '\\');
+    }
+
+    if (dsiString === "") {
+      warnings.push("Given D-SI string is empty!");
+      return {
+        original: 'NULL',
+        tree: [[new DSIUnitNode('', 'NULL', '', false)]],
+        warnings,
+        nonDsiUnit: true
+      };
+    }
+
+    // Parse fractions: split at "\per" (literal string "\per")
+    let modifiedStr = dsiString.replace('percent', 'prozent');
+    let parts = modifiedStr.split('\\per').map(s => s.replace('prozent', 'percent'));
+
+    // Check for empty fraction parts
+    parts = parts.filter(part => {
+      if (part.length === 0) {
+        warnings.push(`The dsi string contains a \\per missing a numerator or denominator! Given string: ${dsiString}`);
+        return false;
+      }
+      return true;
+    });
+    if (parts.length > 2) {
+      warnings.push(`The dsi string contains more than one \\per, does not match specs! Given string: ${dsiString}`);
+    }
+
+    // For each fraction, parse the fractionless part.
+    const tree = parts.map(part => {
+      return this._parseFractionlessDsi(part, warnings);
+    });
+
+    return {
+      original: dsiString,
+      tree,
+      warnings,
+      nonDsiUnit: false
+    };
+  }
+
+  _parseFractionlessDsi(dsiString, warnings) {
+    let items = dsiString.split('\\');
+    if (items[0] === '') {
+      items.shift();
+    } else {
+      warnings.push(`String should start with a backslash, string given was «${dsiString}»`);
+    }
+    const nodes = [];
+    while (items.length) {
+      let prefix = '';
+      let unit = '';
+      let exponent = '';
+      let valid = true;
+
+      if (items.length && items[0] in dsiPrefixesHTML) {
+        prefix = items.shift();
+      }
+      if (items.length) {
+        unit = items.shift();
+      } else {
+        warnings.push(`This D-SI unit seems to be missing the base unit! \\${prefix}`);
+        valid = false;
+      }
+      if (items.length && /^tothe\{[^{}]*\}$/.test(items[0])) {
+        const expToken = items.shift();
+        const match = expToken.match(/tothe\{([^{}]+)\}/);
+        if (match) {
+          exponent = match[1];
+          if (isNaN(Number(exponent))) {
+            warnings.push(`The exponent «${exponent}» is not a number!`);
+            valid = false;
+          }
+        }
+      }
+      // If a unit token was provided but is not valid, warn and mark invalid.
+      if (unit === '') {
+        warnings.push(`This D-SI unit seems to be missing the base unit! \\${prefix}${exponent ? '\\tothe{' + exponent + '}' : ''}`);
+        valid = false;
+      } else if (!(unit in dsiUnitsHTML)) {
+        warnings.push(`The identifier «${unit}» does not match any D-SI units!`);
+        valid = false;
+      }
+      nodes.push(new DSIUnitNode(prefix, unit, exponent, valid));
+    }
+    return nodes;
+  }
+}
+
+export default DSIParser;
diff --git a/dsiUnits-js/src/dsiUnit.js b/dsiUnits-js/src/dsiUnit.js
new file mode 100644
index 0000000000000000000000000000000000000000..9a26986c46e9cd0c50e44d43c57c5e19d3c4dc9c
--- /dev/null
+++ b/dsiUnits-js/src/dsiUnit.js
@@ -0,0 +1,56 @@
+// src/dsiUnit.js
+import DSIParser from './dsiParser.js';
+
+export class DSIUnit {
+  constructor(dsiString) {
+    const parser = DSIParser.getInstance();
+    const result = parser.parse(dsiString);
+    this.dsiString = result.original;
+    this.tree = result.tree;
+    this.warnings = result.warnings;
+    this.nonDsiUnit = result.nonDsiUnit;
+    // For non-DSI units (or empty string), treat them as "valid" for rendering purposes.
+    this.valid = this.nonDsiUnit ? true : (this.warnings.length === 0);
+    this.scaleFactor = 1.0;
+  }
+
+  toHTML(options = {}) {
+    let htmlOutput = '';
+    if (this.nonDsiUnit) {
+      const content = `<span class="dsi-nonunit">${this.dsiString.startsWith('|') ? this.dsiString.slice(1) : this.dsiString}</span>`;
+      htmlOutput = content;
+    } else if (this.tree.length === 1) {
+      htmlOutput = this.tree[0].map(node => node.toHTML(options)).join(options.oneLine ? " " : '<span class="dsi-mul">&nbsp;</span>');
+    } else if (this.tree.length === 2) {
+      if (options.oneLine) {
+        const numerator = this.tree[0].map(node => node.toHTML(options)).join(" ");
+        const denominator = this.tree[1].map(node => node.toHTML(options)).join(" ");
+        htmlOutput = `${numerator} / ${denominator}`;
+      } else {
+        const numerator = this.tree[0].map(node => node.toHTML(options)).join('<span class="dsi-mul">&nbsp;</span>');
+        const denominator = this.tree[1].map(node => node.toHTML(options)).join('<span class="dsi-mul">&nbsp;</span>');
+        htmlOutput = `<span class="dsi-fraction"><span class="dsi-numerator">${numerator}</span><span class="dsi-denom">${denominator}</span></span>`;
+      }
+    } else {
+      htmlOutput = this.tree.map(frac => frac.map(node => node.toHTML(options)).join(options.oneLine ? " " : '<span class="dsi-mul">&nbsp;</span>'))
+          .join(options.oneLine ? " / " : '<span class="dsi-invalid">/</span>');
+    }
+    if (this.scaleFactor !== 1.0) {
+      htmlOutput = `<span class="dsi-scale">${this.scaleFactor}&middot;</span>` + htmlOutput;
+    }
+    if (!this.nonDsiUnit && !this.valid) {
+      htmlOutput = `<span class="dsi-error">${htmlOutput}</span>`;
+    }
+    let tooltip = "";
+    if (!this.nonDsiUnit && this.warnings && this.warnings.length > 0) {
+      const tooltipText = this.warnings.map(w => w.replace(/\\\\/g, '\\')).join("; ");
+      tooltip = ` title="${tooltipText}"`;
+    }
+    return `<span class="dsi-unit-wrapper"${tooltip}>${htmlOutput}</span>`;
+  }
+
+  toString() {
+    if (this.nonDsiUnit) return '|' + this.dsiString;
+    return this.tree.map(frac => frac.map(node => node.toString()).join('')).join('\\per');
+  }
+}
diff --git a/dsiUnits-js/src/dsiUnitInput.js b/dsiUnits-js/src/dsiUnitInput.js
new file mode 100644
index 0000000000000000000000000000000000000000..540daf92e5017e49b9dbe3a2750be99c61836b77
--- /dev/null
+++ b/dsiUnits-js/src/dsiUnitInput.js
@@ -0,0 +1,330 @@
+// src/dsiUnitInput.js
+import { DSIUnit } from "./dsiUnit.js";
+import { dsiPrefixesHTML, dsiUnitsHTML, dsiKeyWords } from "./unitStrings.js";
+
+// Build a default suggestion list from prefixes, units, and keywords.
+const defaultAllowedTokens = [
+    ...Object.keys(dsiPrefixesHTML),
+    ...Object.keys(dsiUnitsHTML),
+    "per",
+    "tothe"
+].sort();
+
+export class DSIUnitInput extends HTMLElement {
+    constructor() {
+        super();
+        this.attachShadow({ mode: "open" });
+        // Internal state.
+        this._rawValue = "";
+        this.liveUpdate = true;
+        this.allowedTokens = defaultAllowedTokens;
+        this.selectedSuggestionIndex = -1;
+        this.lastAcceptedToken = ""; // Tracks the last accepted token
+
+        // Create elements.
+        this.input = document.createElement("input");
+        this.input.type = "text";
+        this.input.id = "dsiInput";
+
+        this.suggestions = document.createElement("div");
+        this.suggestions.className = "autocomplete-list";
+        this.suggestions.style.display = "none";
+
+        this.display = document.createElement("div"); // Rendered output on blur.
+        this.display.id = "dsiDisplay";
+        this.display.style.cursor = "text"; // Clickable for editing.
+
+        // Container for input and suggestions.
+        const container = document.createElement("div");
+        container.style.position = "relative";
+        container.appendChild(this.input);
+        container.appendChild(this.suggestions);
+        container.appendChild(this.display);
+
+        // Append styles.
+        const style = document.createElement("style");
+        style.textContent = `
+      #dsiInput {
+        width: 400px;
+        font-family: monospace;
+        padding: 4px;
+        box-sizing: border-box;
+      }
+      .autocomplete-list {
+        position: absolute;
+        border: 1px solid #ccc;
+        background: #fff;
+        max-height: 150px;
+        overflow-y: auto;
+        width: 400px;
+        z-index: 100;
+      }
+      .autocomplete-item {
+        padding: 4px;
+        cursor: pointer;
+      }
+      .autocomplete-item.selected {
+        background-color: #bde4ff;
+      }
+      #dsiDisplay {
+        margin-top: 5px;
+        padding: 4px;
+        border: 1px solid #ccc;
+        display: none;
+        width: 400px;
+        font-family: monospace;
+      }
+    `;
+        this.shadowRoot.append(style, container);
+
+        // Bind event handlers.
+        this.onInput = this.onInput.bind(this);
+        this.onKeyDown = this.onKeyDown.bind(this);
+        this.onBlur = this.onBlur.bind(this);
+        this.onFocus = this.onFocus.bind(this);
+    }
+
+    static get observedAttributes() {
+        return ["suggestions-list"];
+    }
+
+    attributeChangedCallback(name, oldValue, newValue) {
+        if (name === "suggestions-list") {
+            if (newValue) {
+                this.allowedTokens = newValue.split(",").map(s => s.trim());
+            } else {
+                this.allowedTokens = defaultAllowedTokens;
+            }
+        }
+    }
+
+    connectedCallback() {
+        const suggestionsAttr = this.getAttribute("suggestions-list");
+        if (suggestionsAttr) {
+            this.allowedTokens = suggestionsAttr.split(",").map(s => s.trim());
+        }
+        this.input.addEventListener("input", this.onInput);
+        this.input.addEventListener("keydown", this.onKeyDown);
+        this.input.addEventListener("blur", this.onBlur);
+        this.input.addEventListener("focus", this.onFocus);
+        this.display.addEventListener("click", this.onFocus);
+    }
+
+    disconnectedCallback() {
+        this.input.removeEventListener("input", this.onInput);
+        this.input.removeEventListener("keydown", this.onKeyDown);
+        this.input.removeEventListener("blur", this.onBlur);
+        this.input.removeEventListener("focus", this.onFocus);
+        this.display.removeEventListener("click", this.onFocus);
+    }
+
+    // Determines contextual suggestions.
+    getContextualSuggestions() {
+        const value = this.input.value;
+        // Trim the value to remove extra whitespace.
+        const tokens = value.trim().split("\\").filter(t => t !== "");
+        // If no complete tokens, return all.
+        if (tokens.length === 0) {
+            return this.allowedTokens;
+        }
+        // If a last accepted token exists and is a prefix, use that.
+        if (this.lastAcceptedToken && (this.lastAcceptedToken in dsiPrefixesHTML)) {
+            return Object.keys(dsiUnitsHTML);
+        }
+        // Otherwise, use the previous token from the current input.
+        const prevToken = tokens.length >= 2 ? tokens[tokens.length - 2] : "<empty>";
+        if (prevToken in dsiPrefixesHTML) {
+            return Object.keys(dsiUnitsHTML);
+        } else if (prevToken in dsiUnitsHTML) {
+            if (value.includes("\\per")) {
+                return this.allowedTokens.filter(token => token !== "per");
+            }
+            return this.allowedTokens;
+        } else if (prevToken === "per" || prevToken === "tothe") {
+            return this.allowedTokens;
+        } else {
+            return this.allowedTokens;
+        }
+    }
+
+    onInput(e) {
+        this._rawValue = this.input.value;
+        // Auto-insert a backslash after a complete token if space is typed.
+        if (this.input.value.endsWith(" ")) {
+            const trimmed = this.input.value.trimEnd();
+            if (!trimmed.endsWith("\\")) {
+                this.input.value = trimmed + "\\";
+                this._rawValue = this.input.value;
+            }
+        }
+        this.selectedSuggestionIndex = -1;
+        this.updateSuggestions();
+        if (this.liveUpdate) {
+            this.renderOutput();
+        }
+    }
+
+    updateSuggestions() {
+        const cursorPos = this.input.selectionStart;
+        const value = this.input.value;
+        // Do not show suggestions if caret is inside tothe braces.
+        const regexTothe = /\\tothe\{([^}]*)$/;
+        const substring = value.substring(0, cursorPos);
+        const matchTothe = substring.match(regexTothe);
+        if (matchTothe) {
+            console.log("State: inside tothe braces → no suggestions");
+            this.suggestions.style.display = "none";
+            return;
+        }
+        // Capture current token (which may be empty).
+        const regex = /\\([a-zA-Z]*)$/;
+        const match = substring.match(regex);
+        let currentToken = "";
+        if (match) {
+            currentToken = match[1];
+        }
+        // If current token exactly matches a known prefix, auto-complete it.
+        if (currentToken && (currentToken in dsiPrefixesHTML)) {
+            console.log("Token complete: using accepted prefix", currentToken);
+            this.lastAcceptedToken = currentToken;
+            // Auto-append trailing backslash if not present.
+            if (!value.endsWith("\\")) {
+                this.input.value = value + "\\";
+                this._rawValue = this.input.value;
+            }
+            currentToken = "";
+        }
+        const tokens = value.trim().split("\\").filter(t => t !== "");
+        let prevToken = tokens.length >= 2 ? tokens[tokens.length - 2] : (this.lastAcceptedToken || "<empty>");
+        const contextSuggestions = this.getContextualSuggestions();
+        let filtered = currentToken === ""
+            ? contextSuggestions
+            : contextSuggestions.filter(token => token.startsWith(currentToken));
+        if (filtered.length === 0) {
+            console.log("State: prevToken =", prevToken, "currentToken =", currentToken, "→ No suggestions (filtered empty)");
+            this.suggestions.style.display = "none";
+            return;
+        }
+        this.selectedSuggestionIndex = 0;
+        console.log("State: prevToken =", prevToken, "currentToken =", currentToken, "contextSuggestions =", contextSuggestions, "filtered =", filtered);
+        this.showSuggestions(filtered, value.length - currentToken.length);
+    }
+
+    showSuggestions(list, tokenStartIndex) {
+        this.suggestions.innerHTML = "";
+        if (list.length === 0) {
+            this.suggestions.style.display = "none";
+            return;
+        }
+        list.forEach((token, index) => {
+            const item = document.createElement("div");
+            item.textContent = "\\" + token;
+            item.className = "autocomplete-item";
+            if (index === this.selectedSuggestionIndex) {
+                item.classList.add("selected");
+            }
+            item.addEventListener("mousedown", (e) => {
+                e.preventDefault();
+                this.acceptSuggestion(token, tokenStartIndex);
+            });
+            this.suggestions.appendChild(item);
+        });
+        this.suggestions.style.display = "block";
+    }
+
+    onKeyDown(e) {
+        if (this.suggestions.style.display !== "none") {
+            const items = this.suggestions.querySelectorAll(".autocomplete-item");
+            if (e.key === "ArrowDown") {
+                e.preventDefault();
+                if (items.length > 0) {
+                    this.selectedSuggestionIndex = (this.selectedSuggestionIndex + 1) % items.length;
+                    this.updateSuggestionSelection();
+                }
+            } else if (e.key === "ArrowUp") {
+                e.preventDefault();
+                if (items.length > 0) {
+                    this.selectedSuggestionIndex = (this.selectedSuggestionIndex - 1 + items.length) % items.length;
+                    this.updateSuggestionSelection();
+                }
+            } else if (["Enter", "Tab", "ArrowRight"].includes(e.key)) {
+                if (this.selectedSuggestionIndex !== -1) {
+                    e.preventDefault();
+                    const selectedItem = items[this.selectedSuggestionIndex];
+                    const token = selectedItem.textContent.slice(1); // remove leading backslash
+                    const tokenStartIndex = this.input.value.lastIndexOf("\\");
+                    this.acceptSuggestion(token, tokenStartIndex);
+                }
+            }
+        }
+    }
+
+    updateSuggestionSelection() {
+        const items = this.suggestions.querySelectorAll(".autocomplete-item");
+        items.forEach((item, idx) => {
+            if (idx === this.selectedSuggestionIndex) {
+                item.classList.add("selected");
+            } else {
+                item.classList.remove("selected");
+            }
+        });
+    }
+
+    acceptSuggestion(token, tokenStartIndex) {
+        const value = this.input.value;
+        const before = value.substring(0, tokenStartIndex);
+        const after = value.substring(this.input.selectionStart);
+        if (token === "tothe") {
+            this.input.value = before + "\\" + token + "{}" + after;
+            this._rawValue = this.input.value;
+            this.lastAcceptedToken = token;
+            const newPos = before.length + token.length + 2;
+            this.input.setSelectionRange(newPos, newPos);
+        } else {
+            this.input.value = before + "\\" + token + after;
+            this._rawValue = this.input.value;
+            this.lastAcceptedToken = token;
+        }
+        this.suggestions.style.display = "none";
+        this.renderOutput();
+    }
+
+    renderOutput() {
+        try {
+            const unit = new DSIUnit(this.input.value);
+            this.display.innerHTML = unit.toHTML({ oneLine: true });
+            if (unit.warnings && unit.warnings.length > 0) {
+                this.display.title = unit.warnings.join("; ");
+            } else {
+                this.display.removeAttribute("title");
+            }
+        } catch (err) {
+            this.display.innerHTML = `<span style="color:red;">Error: ${err.message}</span>`;
+        }
+    }
+
+    onBlur(e) {
+        this.renderOutput();
+        this.input.style.display = "none";
+        this.display.style.display = "block";
+        this.suggestions.style.display = "none";
+    }
+
+    onFocus(e) {
+        this.input.style.display = "block";
+        this.display.style.display = "none";
+        this.input.value = this._rawValue;
+        this.input.focus();
+        this.input.setSelectionRange(this.input.value.length, this.input.value.length);
+    }
+
+    get value() {
+        return this._rawValue;
+    }
+
+    set live(value) {
+        this.liveUpdate = Boolean(value);
+    }
+}
+
+customElements.define("dsi-unit-input", DSIUnitInput);
diff --git a/dsiUnits-js/src/dsiUnitNode.js b/dsiUnits-js/src/dsiUnitNode.js
new file mode 100644
index 0000000000000000000000000000000000000000..5d3d6fdf9b087c1ff14798dfb4f8631805f266c5
--- /dev/null
+++ b/dsiUnits-js/src/dsiUnitNode.js
@@ -0,0 +1,71 @@
+// src/dsiUnitNode.js
+import { dsiPrefixesHTML, dsiUnitsHTML } from "./unitStrings.js";
+
+export class DSIUnitNode {
+  constructor(prefix, unit, exponent = '', valid = true, scaleFactor = 1.0) {
+    this.prefix = prefix;
+    this.unit = unit;
+    this.valid = valid;
+    if (exponent === '' || exponent === '1') {
+      this.exponent = 1;
+    } else {
+      const numExp = Number(exponent);
+      this.exponent = isNaN(numExp) ? exponent : numExp;
+    }
+    this.scaleFactor = scaleFactor;
+  }
+
+  toHTML(options = {}) {
+    let prefixHTML = '';
+    if (this.prefix) {
+      if (this.prefix in dsiPrefixesHTML) {
+        prefixHTML = `<span class="dsi-unit">${dsiPrefixesHTML[this.prefix]}</span>`;
+      } else {
+        prefixHTML = `<span class="dsi-unit"><span class="dsi-invalid">${this.prefix}</span></span>`;
+      }
+    }
+    let unitHTML = '';
+    if (this.unit) {
+      if (this.unit in dsiUnitsHTML) {
+        unitHTML = `<span class="dsi-unit">${dsiUnitsHTML[this.unit]}</span>`;
+      } else {
+        unitHTML = `<span class="dsi-unit"><span class="dsi-invalid">${this.unit}</span></span>`;
+      }
+    }
+    let baseHTML = prefixHTML + unitHTML;
+    if (this.exponent !== 1) {
+      if (typeof this.exponent === 'number') {
+        if (Math.abs(this.exponent - 0.5) < 0.0001) {
+          // Square root.
+          return options.oneLine ? `√(${baseHTML})`
+              : `<span class="dsi-root"><span class="dsi-root-symbol">√</span><span class="dsi-root-content">${baseHTML}</span></span>`;
+        } else if (Math.abs(this.exponent - 0.333333) < 0.0001) {
+          // Cube root.
+          return options.oneLine ? `∛(${baseHTML})`
+              : `<span class="dsi-root"><span class="dsi-root-symbol">∛</span><span class="dsi-root-content">${baseHTML}</span></span>`;
+        } else if (Math.abs(this.exponent - 0.25) < 0.0001) {
+          // Fourth root.
+          return options.oneLine ? `∜(${baseHTML})`
+              : `<span class="dsi-root"><span class="dsi-root-symbol">∜</span><span class="dsi-root-content">${baseHTML}</span></span>`;
+        } else {
+          return options.oneLine
+              ? `${baseHTML}^(${this.exponent})`
+              : `${baseHTML}<sup class="dsi-exponent">${this.exponent}</sup>`;
+        }
+      } else {
+        return options.oneLine
+            ? `${baseHTML}^(<span class="dsi-invalid">${this.exponent}</span>)`
+            : `${baseHTML}<sup class="dsi-exponent"><span class="dsi-invalid">${this.exponent}</span></sup>`;
+      }
+    }
+    return baseHTML;
+  }
+
+  toString() {
+    let str = '';
+    if (this.prefix) str += '\\' + this.prefix;
+    str += '\\' + this.unit;
+    if (this.exponent !== 1) str += `\\tothe{${this.exponent}}`;
+    return str;
+  }
+}
diff --git a/dsiUnits-js/src/index.js b/dsiUnits-js/src/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..6870173249a587b1834ceb59e14a7cabfe4031ef
--- /dev/null
+++ b/dsiUnits-js/src/index.js
@@ -0,0 +1,3 @@
+// src/index.js
+export { DSIUnit } from "./dsiUnit.js";
+export { DSIUnitInput } from "./dsiUnitInput.js";
\ No newline at end of file
diff --git a/dsiUnits-js/src/unitStrings.js b/dsiUnits-js/src/unitStrings.js
new file mode 100644
index 0000000000000000000000000000000000000000..b9ff8c0a041cfeb844c306ccaa25d5a2aab75a0e
--- /dev/null
+++ b/dsiUnits-js/src/unitStrings.js
@@ -0,0 +1,92 @@
+// unitStrings.js
+export const dsiPrefixesHTML = {
+  deca: 'da',
+  hecto: 'h',
+  kilo: 'k',
+  mega: 'M',
+  giga: 'G',
+  tera: 'T',
+  peta: 'P',
+  exa: 'E',
+  zetta: 'Z',
+  yotta: 'Y',
+  deci: 'd',
+  centi: 'c',
+  milli: 'm',
+  micro: 'µ',
+  nano: 'n',
+  pico: 'p',
+  femto: 'f',
+  atto: 'a',
+  zepto: 'z',
+  yocto: 'y',
+  kibi: 'Ki',
+  mebi: 'Mi',
+  gibi: 'Gi',
+  tebi: 'Ti',
+  pebi: 'Pi',
+  exbi: 'Ei',
+  zebi: 'Zi',
+  yobi: 'Yi'
+};
+
+export const dsiUnitsHTML = {
+  metre: 'm',
+  kilogram: 'kg',
+  second: 's',
+  ampere: 'A',
+  kelvin: 'K',
+  mole: 'mol',
+  candela: 'cd',
+  one: '1',
+  day: 'd',
+  hour: 'h',
+  minute: 'min',
+  degree: '°',
+  arcminute: '′',
+  arcsecond: '″',
+  gram: 'g',
+  radian: 'rad',
+  steradian: 'sr',
+  hertz: 'Hz',
+  newton: 'N',
+  pascal: 'Pa',
+  joule: 'J',
+  watt: 'W',
+  coulomb: 'C',
+  volt: 'V',
+  farad: 'F',
+  ohm: 'Ω',
+  siemens: 'S',
+  weber: 'Wb',
+  tesla: 'T',
+  henry: 'H',
+  degreecelsius: '°C',
+  lumen: 'lm',
+  lux: 'lx',
+  becquerel: 'Bq',
+  sievert: 'Sv',
+  gray: 'Gy',
+  katal: 'kat',
+  hectare: 'ha',
+  litre: 'l',
+  tonne: 't',
+  electronvolt: 'eV',
+  dalton: 'Da',
+  astronomicalunit: 'au',
+  neper: 'Np',
+  bel: 'B',
+  decibel: 'dB',
+  percent: '%',
+  ppm: 'ppm',
+  byte: 'Byte',
+  bit: 'bit',
+  angstrom: 'Å',
+  bar: 'bar',
+  // Extend as needed...
+};
+
+export const dsiKeyWords = {
+  tothe: '\\tothe',
+  per: '\\per'
+};
diff --git a/dsiUnits-js/tests/dsiUnitInput.test.js b/dsiUnits-js/tests/dsiUnitInput.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..ab1eccc605466f47cc266e8f42f51be57486816c
--- /dev/null
+++ b/dsiUnits-js/tests/dsiUnitInput.test.js
@@ -0,0 +1,65 @@
+/**
+ * @jest-environment jsdom
+ */
+import "../src/dsiUnitInput.js";
+import { DSIUnit } from "../src/dsiUnit.js";
+
+describe("DSIUnitInput custom element", () => {
+    let element;
+    beforeEach(() => {
+        document.body.innerHTML = `<dsi-unit-input></dsi-unit-input>`;
+        element = document.querySelector("dsi-unit-input");
+    });
+
+    test("initializes with an input field", () => {
+        const input = element.shadowRoot.querySelector("#dsiInput");
+        expect(input).not.toBeNull();
+    });
+
+    test("shows suggestions after typing backslash and letters", () => {
+        const input = element.shadowRoot.querySelector("#dsiInput");
+        input.value = "\\m";
+        input.dispatchEvent(new Event("input"));
+        const suggestions = element.shadowRoot.querySelectorAll(".autocomplete-item");
+        expect(suggestions.length).toBeGreaterThan(0);
+        suggestions.forEach(item => {
+            expect(item.textContent).toMatch(/^\\m/);
+        });
+    });
+
+    test("on blur, input is replaced by rendered output", () => {
+        const input = element.shadowRoot.querySelector("#dsiInput");
+        const display = element.shadowRoot.querySelector("#dsiDisplay");
+        input.value = "\\metre";
+        input.dispatchEvent(new Event("input"));
+        input.dispatchEvent(new Event("blur"));
+        expect(input.style.display).toBe("none");
+        expect(display.style.display).toBe("block");
+        const unit = new DSIUnit("\\metre");
+        expect(display.innerHTML).toBe(unit.toHTML());
+    });
+
+    test("on focus, raw value is restored", () => {
+        const input = element.shadowRoot.querySelector("#dsiInput");
+        input.value = "\\metre\\tothe{2}";
+        input.dispatchEvent(new Event("input"));
+        input.dispatchEvent(new Event("blur"));
+        input.dispatchEvent(new Event("focus"));
+        expect(input.style.display).toBe("block");
+        expect(input.value).toBe("\\metre\\tothe{2}");
+    });
+
+    test("external suggestions list overrides defaults", () => {
+        // Set the attribute "suggestions-list" to only allow "foo" and "bar"
+        element.setAttribute("suggestions-list", "foo,bar");
+        // Force re-read of the attribute by calling attributeChangedCallback manually if needed.
+        const input = element.shadowRoot.querySelector("#dsiInput");
+        input.value = "\\f";
+        input.dispatchEvent(new Event("input"));
+        const suggestions = element.shadowRoot.querySelectorAll(".autocomplete-item");
+        expect(suggestions.length).toBeGreaterThan(0);
+        suggestions.forEach(item => {
+            expect(item.textContent).toMatch(/^\\(foo|bar)$/);
+        });
+    });
+});
diff --git a/dsiUnits-js/tests/dsiUnits.test.js b/dsiUnits-js/tests/dsiUnits.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..e5069d3e2d65023f4e8f8bbd5beac4d847c481cb
--- /dev/null
+++ b/dsiUnits-js/tests/dsiUnits.test.js
@@ -0,0 +1,63 @@
+// tests/dsiUnits.test.js
+import { DSIUnit } from '../src/dsiUnit.js';
+
+describe('DSIUnit HTML Output Tests', () => {
+  test('Base case: one unit without prefix or exponent', () => {
+    const unit = new DSIUnit('\\metre');
+    expect(unit.tree.length).toBe(1);
+    expect(unit.tree[0].length).toBe(1);
+    expect(unit.tree[0][0].unit).toBe('metre');
+    expect(unit.tree[0][0].exponent).toBe(1);
+    expect(unit.toHTML()).toBe('<span class="dsi-unit-wrapper"><span class="dsi-unit">m</span></span>');
+    expect(unit.valid).toBe(true);
+    expect(unit.warnings).toEqual([]);
+  });
+
+  test('Exponent handling: metre^2 and square-root', () => {
+    const unit2 = new DSIUnit('\\metre\\tothe{2}');
+    expect(unit2.toHTML()).toBe('<span class="dsi-unit-wrapper"><span class="dsi-unit">m</span><sup class="dsi-exponent">2</sup></span>');
+
+    const unitSqrt = new DSIUnit('\\metre\\tothe{0.5}');
+    expect(unitSqrt.toHTML()).toBe('<span class="dsi-unit-wrapper"><span class="dsi-root"><span class="dsi-root-symbol">√</span><span class="dsi-root-content"><span class="dsi-unit">m</span></span></span></span>');
+  });
+
+  test('Fraction: e.g. mega metre per second^2', () => {
+    const unitFrac = new DSIUnit('\\mega\\metre\\per\\second\\tothe{2}');
+    const expectedHTML = '<span class="dsi-unit-wrapper"><span class="dsi-fraction"><span class="dsi-numerator"><span class="dsi-unit">M</span><span class="dsi-unit">m</span></span><span class="dsi-denom"><span class="dsi-unit">s</span><sup class="dsi-exponent">2</sup></span></span></span>';
+    expect(unitFrac.toHTML()).toBe(expectedHTML);
+  });
+
+  test('Robustness: unknown unit', () => {
+    const unitUnknown = new DSIUnit('\\foo');
+    const expectedHTML = '<span class="dsi-unit-wrapper" title="The identifier «foo» does not match any D-SI units!"><span class="dsi-error"><span class="dsi-unit"><span class="dsi-invalid">foo</span></span></span></span>';
+    expect(unitUnknown.toHTML()).toBe(expectedHTML);
+    expect(unitUnknown.valid).toBe(false);
+    expect(unitUnknown.warnings).toContain('The identifier «foo» does not match any D-SI units!');
+  });
+
+  test('Non D-SI unit marker', () => {
+    const nonDsi = new DSIUnit('|NonDsiUnit');
+    const expectedHTML = '<span class="dsi-unit-wrapper"><span class="dsi-nonunit">NonDsiUnit</span></span>';
+    expect(nonDsi.toHTML()).toBe(expectedHTML);
+  });
+
+  test('Empty string', () => {
+    const emptyUnit = new DSIUnit('');
+    const expectedHTML = '<span class="dsi-unit-wrapper"><span class="dsi-nonunit">NULL</span></span>';
+    expect(emptyUnit.toHTML()).toBe(expectedHTML);
+    expect(emptyUnit.warnings).toContain('Given D-SI string is empty!');
+  });
+
+  test('Double backslash handling', () => {
+    const unitDouble = new DSIUnit('\\\\metre\\per\\second');
+    const expectedHTML = '<span class="dsi-unit-wrapper" title="Double backslash found in string, treating as one backslash: «\\metre\\per\\second»"><span class="dsi-error"><span class="dsi-fraction"><span class="dsi-numerator"><span class="dsi-unit">m</span></span><span class="dsi-denom"><span class="dsi-unit">s</span></span></span></span></span>';
+    expect(unitDouble.toHTML()).toBe(expectedHTML);
+    expect(unitDouble.warnings.some(w => w.includes('Double backslash'))).toBe(true);
+  });
+
+  test('Fourth root: micro metre tothe{0.25}', () => {
+    const unitFourth = new DSIUnit('\\micro\\metre\\tothe{0.25}');
+    const expectedHTML = '<span class="dsi-unit-wrapper"><span class="dsi-root"><span class="dsi-root-symbol">∜</span><span class="dsi-root-content"><span class="dsi-unit">µ</span><span class="dsi-unit">m</span></span></span></span>';
+    expect(unitFourth.toHTML()).toBe(expectedHTML);
+  });
+});
diff --git a/dsiUnits-js/tests/visual_test.html b/dsiUnits-js/tests/visual_test.html
new file mode 100644
index 0000000000000000000000000000000000000000..52ac585b4badac379b7c799fadf482bf6468cbd7
--- /dev/null
+++ b/dsiUnits-js/tests/visual_test.html
@@ -0,0 +1,284 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>D-SI Units Visual Test & Documentation</title>
+    <style>
+        body {
+            font-family: sans-serif;
+            margin: 20px;
+            line-height: 1.6;
+        }
+        h1, h2, h3 {
+            color: #333;
+        }
+        table {
+            border-collapse: collapse;
+            width: 100%;
+            margin-bottom: 40px;
+        }
+        th, td {
+            border: 1px solid #ccc;
+            padding: 8px;
+            text-align: left;
+            vertical-align: top;
+        }
+        th {
+            background-color: #eee;
+        }
+        pre {
+            margin: 0;
+            font-family: monospace;
+            background: #f8f8f8;
+            padding: 4px;
+        }
+        .dsi-invalid {
+            color: red;
+            font-weight: bold;
+        }
+        .dsi-error {
+            color: orange;
+        }
+        .dsi-fraction {
+            display: inline-block;
+            text-align: center;
+            vertical-align: middle;
+        }
+        .dsi-numerator {
+            display: block;
+            border-bottom: 1px solid #000;
+            padding: 0 2px;
+            line-height: 1.2;
+        }
+        .dsi-denom {
+            display: block;
+            padding: 0 2px;
+            line-height: 1.2;
+        }
+        .dsi-root {
+            display: inline-flex;
+            align-items: flex-start;
+        }
+        .dsi-root-symbol {
+            font-size: 1.2em;
+            padding-right: 2px;
+        }
+        .dsi-root-content {
+            border-top: 1px solid #000;
+            display: inline-block;
+            padding-top: 2px;
+        }
+        .dsi-mul {
+            padding: 0 2px;
+        }
+        .dsi-unit-wrapper {}
+        #documentation {
+            margin-bottom: 40px;
+            padding: 10px;
+            border: 1px solid #ccc;
+            background-color: #fafafa;
+        }
+        .doc-table {
+            margin-bottom: 20px;
+        }
+        .doc-table th {
+            background-color: #ddd;
+        }
+        /* Styles for our custom auto-complete input */
+        dsi-unit-input {
+            display: block;
+            margin-bottom: 40px;
+        }
+        .autocomplete-item.selected {
+            background-color: #bde4ff;
+        }
+    </style>
+</head>
+<body>
+<h1>D-SI Units Visual Test & Documentation</h1>
+
+<div id="documentation">
+    <h2>How D-SI Units Are Composed</h2>
+    <p>
+        D-SI units are represented as strings where each component is preceded by a backslash (<code>\</code>).
+        A basic D-SI unit string is composed of:
+    </p>
+    <ul>
+        <li>An <strong>optional prefix</strong> (e.g. <code>\kilo</code> for 10³, <code>\milli</code> for 10⁻³),</li>
+        <li>A <strong>base unit</strong> (e.g. <code>\metre</code>, <code>\second</code>),</li>
+        <li>An optional exponent specified with <code>\tothe{...}</code> (e.g. <code>\metre\tothe{2}</code> for square metre),
+            with fractional exponents like 0.5, 0.333333, or 0.25 rendered as square, cube, or fourth roots respectively.</li>
+        <li>An optional fraction operator <code>\per</code> is used to combine units into a fraction (e.g. <code>\metre\per\second</code> for metres per second).</li>
+    </ul>
+    <p>
+        Use the auto-complete input below to type a D-SI string. After typing a backslash and at least one letter,
+        suggestions will appear based on the allowed tokens. When the field loses focus, the rendered unit is shown (while
+        the raw string is preserved for further editing when focused again). External suggestion lists can also be provided.
+    </p>
+</div>
+
+<!-- New auto-complete input component -->
+<h2>Interactive D-SI Input</h2>
+<dsi-unit-input suggestions-list="kilo,mega,milli,metre,second,per,tothe"></dsi-unit-input>
+
+<h2>Allowed Prefixes</h2>
+<table id="prefixes-table" class="doc-table">
+    <thead>
+    <tr>
+        <th>Prefix Name</th>
+        <th>Symbol</th>
+    </tr>
+    </thead>
+    <tbody></tbody>
+</table>
+
+<h2>Allowed Units</h2>
+<table id="units-table" class="doc-table">
+    <thead>
+    <tr>
+        <th>Unit Name</th>
+        <th>Symbol</th>
+    </tr>
+    </thead>
+    <tbody></tbody>
+</table>
+
+<h2>Example D-SI Strings</h2>
+<h3>Valid D-SI Strings</h3>
+<table id="valid-table">
+    <thead>
+    <tr>
+        <th>Raw D-SI String</th>
+        <th>Rendered Output</th>
+    </tr>
+    </thead>
+    <tbody></tbody>
+</table>
+
+<h3>Invalid D-SI Strings</h3>
+<table id="invalid-table">
+    <thead>
+    <tr>
+        <th>Raw D-SI String</th>
+        <th>Rendered Output</th>
+        <th>Warnings</th>
+    </tr>
+    </thead>
+    <tbody></tbody>
+</table>
+
+<!-- Existing interactive section (button controlled) -->
+<h2>Interactive Renderer (Button Controlled)</h2>
+<div id="interactiveSection">
+    <label for="dsiInput">Enter D-SI string:</label>
+    <input type="text" id="dsiInput" placeholder="e.g. \metre\tothe{0.333333}">
+    <button id="renderBtn">Render</button>
+    <div id="interactiveResult"></div>
+</div>
+
+<script type="module">
+    import { dsiPrefixesHTML, dsiUnitsHTML } from "../src/unitStrings.js";
+    import { DSIUnit } from "../src/dsiUnit.js";
+    import "../src/dsiUnitInput.js";
+
+    // Populate Allowed Prefixes table.
+    const prefixesTbody = document.getElementById('prefixes-table').querySelector('tbody');
+    Object.keys(dsiPrefixesHTML).sort().forEach(prefix => {
+        const tr = document.createElement('tr');
+        const tdName = document.createElement('td');
+        tdName.textContent = prefix;
+        const tdSymbol = document.createElement('td');
+        tdSymbol.textContent = dsiPrefixesHTML[prefix];
+        tr.appendChild(tdName);
+        tr.appendChild(tdSymbol);
+        prefixesTbody.appendChild(tr);
+    });
+
+    // Populate Allowed Units table.
+    const unitsTbody = document.getElementById('units-table').querySelector('tbody');
+    Object.keys(dsiUnitsHTML).sort().forEach(unit => {
+        const tr = document.createElement('tr');
+        const tdName = document.createElement('td');
+        tdName.textContent = unit;
+        const tdSymbol = document.createElement('td');
+        tdSymbol.textContent = dsiUnitsHTML[unit];
+        tr.appendChild(tdName);
+        tr.appendChild(tdSymbol);
+        unitsTbody.appendChild(tr);
+    });
+
+    // Example D-SI strings.
+    const validDSI = [
+        '\\metre',
+        '\\metre\\tothe{2}',
+        '\\metre\\tothe{0.5}',
+        '\\mega\\metre\\per\\second\\tothe{2}',
+        '\\kilo\\metre\\per\\hour',
+        '\\metre\\per\\second',
+        '\\metre\\tothe{0.333333}', // Cube root
+        '\\metre\\tothe{0.25}'      // Fourth root
+    ];
+    const invalidDSI = [
+        '\\foo',
+        '\\milli\\tothe{2}',
+        '\\metre\\per',
+        '\\\\metre\\per\\second'
+    ];
+
+    function renderDSI(rawStr) {
+        try {
+            const dsi = new DSIUnit(rawStr);
+            return {
+                html: dsi.toHTML(),
+                warnings: dsi.warnings
+            };
+        } catch (error) {
+            return {
+                html: `<span style="color:red;">Error: ${error.message}</span>`,
+                warnings: [error.message]
+            };
+        }
+    }
+
+    function populateTable(tableId, dsiStrings) {
+        const tbody = document.getElementById(tableId).querySelector('tbody');
+        dsiStrings.forEach(rawStr => {
+            const result = renderDSI(rawStr);
+            const tr = document.createElement('tr');
+            const tdRaw = document.createElement('td');
+            tdRaw.innerHTML = `<pre>${rawStr}</pre>`;
+            tr.appendChild(tdRaw);
+            const tdRendered = document.createElement('td');
+            tdRendered.innerHTML = result.html;
+            tr.appendChild(tdRendered);
+            if (tableId === 'invalid-table') {
+                const tdWarnings = document.createElement('td');
+                tdWarnings.innerHTML = result.warnings.join('<br>');
+                tr.appendChild(tdWarnings);
+            }
+            tbody.appendChild(tr);
+        });
+    }
+
+    populateTable('valid-table', validDSI);
+    populateTable('invalid-table', invalidDSI);
+
+    // Button-controlled interactive section.
+    const renderBtn = document.getElementById('renderBtn');
+    renderBtn.addEventListener('click', () => {
+        const inputStr = document.getElementById('dsiInput').value;
+        try {
+            const unit = new DSIUnit(inputStr);
+            const renderedHTML = unit.toHTML();
+            let warningsHTML = "";
+            if (unit.warnings.length > 0) {
+                warningsHTML = `<div id="warningText">Warnings: ${unit.warnings.join("; ")}</div>`;
+            }
+            document.getElementById('interactiveResult').innerHTML = renderedHTML + warningsHTML;
+        } catch (err) {
+            document.getElementById('interactiveResult').innerHTML = `<span style="color:red;">Error: ${err.message}</span>`;
+        }
+    });
+</script>
+</body>
+</html>
diff --git a/pyproject.toml b/pyproject.toml
index 26bff3b7f8179230f9bd3bd74b97b5d2b817c576..4fd46ce5a5b9e8f89afa88aec137c4944ef24e00 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "dsiunits"  # Ensure this is correctly specified
-version = "2.4.0"
+version = "2.4.1"
 description = "This is a Python module for handling the SI units as objects in Python, parsing them from strings and converting them to Latex and Unicode, as well as performing math operations and calculating scale factors."
 authors = [
     { name="Benedikt Seeger", email="benedikt.seeger@ptb.de" },
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index ed5564fcd28318bd1d0224b136f74075a42bc6b0..0000000000000000000000000000000000000000
--- a/setup.cfg
+++ /dev/null
@@ -1,14 +0,0 @@
-[metadata]
-name = dsiUnits
-version = 2.4.0
-description = This is a Python module for handling the SI units as objects in Python, parsing them from strings and converting them to Latex and Unicode, as well as performing math operations and calculating scale factors.
-long_description = file: README.md
-
-[options]
-python_requires = >=3.7
-
-[options.extras_require]
-testing =
-    pytest>=7.4.1
-    pytest-cov>=4.1.0
-    # Any other testing dependencies
diff --git a/src/dsiUnits.py b/src/dsiUnits.py
index 7da858b3779d99b38e0e5bb6747b8bacd96e3958..8af756b903826eea71ef77864fb466f075f73a38 100644
--- a/src/dsiUnits.py
+++ b/src/dsiUnits.py
@@ -588,6 +588,10 @@ class dsiUnit:
                 result += str(node)
         return result
 
+    def __hash__(self):
+        # Use the hash of an immutable attribute (here, self.value)
+        return hash(str(self))
+
     def __repr__(self):
         contentStr = self.toUTF8()
         if not self.valid:
diff --git a/tests/test_dsiUnits.py b/tests/test_dsiUnits.py
index 28f0e652de6ff715ced10334e712572fb1e93387..e1c1a5c360aae0aac06623b61ab56b9fa3023f02 100644
--- a/tests/test_dsiUnits.py
+++ b/tests/test_dsiUnits.py
@@ -618,3 +618,11 @@ def test_exponentMath():
     assert m2.getBaseUnit(km2) == m2
     assert m2.getScaleFactor(km2) == 1.0e6
 
+
+def test_hash():
+    Hmm2=hash(dsiUnit(r'\milli\metre\tothe{2}'))
+    Hkm2=hash(dsiUnit(r'\kilo\metre\tothe{2}'))
+    Hm2=hash(dsiUnit(r'\metre\tothe{2}'))
+    Hm2_2=hash(dsiUnit(r'\metre\tothe{2}'))
+    assert hash(Hmm2) != hash(Hkm2) != hash(Hm2)
+    assert hash(Hm2) == hash(Hm2_2)