diff --git a/application/source/text/util.mjs b/application/source/text/util.mjs
new file mode 100644
index 0000000000000000000000000000000000000000..5c71e7d3b566a72c7cf7c3803b811a5950d9752e
--- /dev/null
+++ b/application/source/text/util.mjs
@@ -0,0 +1,57 @@
+export {generateRangeComparisonExpression}
+
+/**
+ * Generates a comparison expression for a comma-separated string of ranges and single values.
+ * @param {string} expression - The string expression to generate the comparison for.
+ * @param {string} valueName - The name of the value to compare against.
+ * @param {Object} [options] - The optional parameters.
+ * @param {boolean} [options.urlEncode=false] - Whether to encode comparison operators for use in a URL.
+ * @param {string} [options.andOp='&&'] - The logical AND operator to use.
+ * @param {string} [options.orOp='||'] - The logical OR operator to use.
+ * @param {string} [options.eqOp='=='] - The comparison operator for equality to use.
+ * @param {string} [options.geOp='>='] - The comparison operator for greater than or equal to to use.
+ * @param {string} [options.leOp='<='] - The comparison operator for less than or equal to to use.
+ * @returns {string} The generated comparison expression.
+ * @throws {Error} If the input is invalid.
+ */
+function generateRangeComparisonExpression(expression, valueName, options = {}) {
+    const {
+        urlEncode = false,
+        andOp = '&&',
+        orOp = '||',
+        eqOp = '==',
+        geOp = '>=',
+        leOp = '<=',
+    } = options;
+    const ranges = expression.split(',');
+    let comparison = '';
+    for (let i = 0; i < ranges.length; i++) {
+        const range = ranges[i].trim();
+        if (range === '') {
+            throw new Error(`Invalid range '${range}'`);
+        } else if (range.includes('-')) {
+            const [start, end] = range.split('-').map(s => (s === '' ? null : parseFloat(s)));
+            if ((start !== null && isNaN(start)) || (end !== null && isNaN(end))) {
+                throw new Error(`Invalid value in range '${range}'`);
+            }
+            if (start !== null && end !== null && start > end) {
+                throw new Error(`Invalid range '${range}'`);
+            }
+            const compStart = start !== null ? `${valueName}${urlEncode ? encodeURIComponent(geOp) : geOp}${start}` : '';
+            const compEnd = end !== null ? `${valueName}${urlEncode ? encodeURIComponent(leOp) : leOp}${end}` : '';
+            const compRange = `${compStart}${compStart && compEnd ? ` ${andOp} ` : ''}${compEnd}`;
+            comparison += ranges.length > 1 ? `(${compRange})` : compRange;
+        } else {
+            const value = parseFloat(range);
+            if (isNaN(value)) {
+                throw new Error(`Invalid value '${range}'`);
+            }
+            const compValue = `${valueName}${urlEncode ? encodeURIComponent(eqOp) : eqOp}${value}`;
+            comparison += ranges.length > 1 ? `(${compValue})` : compValue;
+        }
+        if (i < ranges.length - 1) {
+            comparison += ` ${orOp} `;
+        }
+    }
+    return comparison;
+}
diff --git a/development/package.json b/development/package.json
index a7e23c32a4fd62359383c8c0e010c96b8760527a..37545862ba008ecc322c63c0a5c43bc22802eb02 100644
--- a/development/package.json
+++ b/development/package.json
@@ -32,7 +32,7 @@
     "create-polyfill-service-url": "^2.2.6",
     "crypt": "^0.0.2",
     "cssnano": "^5.1.15",
-    "esbuild": "^0.17.11",
+    "esbuild": "^0.17.12",
     "flow-bin": "^0.202.0",
     "fs": "0.0.1-security",
     "glob": "^9.3.0",
diff --git a/development/pnpm-lock.yaml b/development/pnpm-lock.yaml
index 88fdf43bbb9ffa2a969178b4cf2eb61ce1729af5..d1159362e999a9e9a9f61d7614cfc1eb7a8eee8f 100644
--- a/development/pnpm-lock.yaml
+++ b/development/pnpm-lock.yaml
@@ -13,7 +13,7 @@ specifiers:
   create-polyfill-service-url: ^2.2.6
   crypt: ^0.0.2
   cssnano: ^5.1.15
-  esbuild: ^0.17.11
+  esbuild: ^0.17.12
   flow-bin: ^0.202.0
   fs: 0.0.1-security
   glob: ^9.3.0
@@ -62,7 +62,7 @@ devDependencies:
   create-polyfill-service-url: 2.2.6
   crypt: 0.0.2
   cssnano: 5.1.15_postcss@8.4.21
-  esbuild: 0.17.11
+  esbuild: 0.17.12
   flow-bin: 0.202.0
   fs: 0.0.1-security
   glob: 9.3.0
@@ -322,8 +322,8 @@ packages:
       postcss-selector-parser: 6.0.11
     dev: true
 
-  /@esbuild/android-arm/0.17.11:
-    resolution: {integrity: sha512-CdyX6sRVh1NzFCsf5vw3kULwlAhfy9wVt8SZlrhQ7eL2qBjGbFhRBWkkAzuZm9IIEOCKJw4DXA6R85g+qc8RDw==}
+  /@esbuild/android-arm/0.17.12:
+    resolution: {integrity: sha512-E/sgkvwoIfj4aMAPL2e35VnUJspzVYl7+M1B2cqeubdBhADV4uPon0KCc8p2G+LqSJ6i8ocYPCqY3A4GGq0zkQ==}
     engines: {node: '>=12'}
     cpu: [arm]
     os: [android]
@@ -331,8 +331,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/android-arm64/0.17.11:
-    resolution: {integrity: sha512-QnK4d/zhVTuV4/pRM4HUjcsbl43POALU2zvBynmrrqZt9LPcLA3x1fTZPBg2RRguBQnJcnU059yKr+bydkntjg==}
+  /@esbuild/android-arm64/0.17.12:
+    resolution: {integrity: sha512-WQ9p5oiXXYJ33F2EkE3r0FRDFVpEdcDiwNX3u7Xaibxfx6vQE0Sb8ytrfQsA5WO6kDn6mDfKLh6KrPBjvkk7xA==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [android]
@@ -340,8 +340,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/android-x64/0.17.11:
-    resolution: {integrity: sha512-3PL3HKtsDIXGQcSCKtWD/dy+mgc4p2Tvo2qKgKHj9Yf+eniwFnuoQ0OUhlSfAEpKAFzF9N21Nwgnap6zy3L3MQ==}
+  /@esbuild/android-x64/0.17.12:
+    resolution: {integrity: sha512-m4OsaCr5gT+se25rFPHKQXARMyAehHTQAz4XX1Vk3d27VtqiX0ALMBPoXZsGaB6JYryCLfgGwUslMqTfqeLU0w==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [android]
@@ -349,8 +349,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/darwin-arm64/0.17.11:
-    resolution: {integrity: sha512-pJ950bNKgzhkGNO3Z9TeHzIFtEyC2GDQL3wxkMApDEghYx5Qers84UTNc1bAxWbRkuJOgmOha5V0WUeh8G+YGw==}
+  /@esbuild/darwin-arm64/0.17.12:
+    resolution: {integrity: sha512-O3GCZghRIx+RAN0NDPhyyhRgwa19MoKlzGonIb5hgTj78krqp9XZbYCvFr9N1eUxg0ZQEpiiZ4QvsOQwBpP+lg==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [darwin]
@@ -358,8 +358,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/darwin-x64/0.17.11:
-    resolution: {integrity: sha512-iB0dQkIHXyczK3BZtzw1tqegf0F0Ab5texX2TvMQjiJIWXAfM4FQl7D909YfXWnB92OQz4ivBYQ2RlxBJrMJOw==}
+  /@esbuild/darwin-x64/0.17.12:
+    resolution: {integrity: sha512-5D48jM3tW27h1qjaD9UNRuN+4v0zvksqZSPZqeSWggfMlsVdAhH3pwSfQIFJwcs9QJ9BRibPS4ViZgs3d2wsCA==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [darwin]
@@ -367,8 +367,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/freebsd-arm64/0.17.11:
-    resolution: {integrity: sha512-7EFzUADmI1jCHeDRGKgbnF5sDIceZsQGapoO6dmw7r/ZBEKX7CCDnIz8m9yEclzr7mFsd+DyasHzpjfJnmBB1Q==}
+  /@esbuild/freebsd-arm64/0.17.12:
+    resolution: {integrity: sha512-OWvHzmLNTdF1erSvrfoEBGlN94IE6vCEaGEkEH29uo/VoONqPnoDFfShi41Ew+yKimx4vrmmAJEGNoyyP+OgOQ==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [freebsd]
@@ -376,8 +376,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/freebsd-x64/0.17.11:
-    resolution: {integrity: sha512-iPgenptC8i8pdvkHQvXJFzc1eVMR7W2lBPrTE6GbhR54sLcF42mk3zBOjKPOodezzuAz/KSu8CPyFSjcBMkE9g==}
+  /@esbuild/freebsd-x64/0.17.12:
+    resolution: {integrity: sha512-A0Xg5CZv8MU9xh4a+7NUpi5VHBKh1RaGJKqjxe4KG87X+mTjDE6ZvlJqpWoeJxgfXHT7IMP9tDFu7IZ03OtJAw==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [freebsd]
@@ -385,8 +385,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/linux-arm/0.17.11:
-    resolution: {integrity: sha512-M9iK/d4lgZH0U5M1R2p2gqhPV/7JPJcRz+8O8GBKVgqndTzydQ7B2XGDbxtbvFkvIs53uXTobOhv+RyaqhUiMg==}
+  /@esbuild/linux-arm/0.17.12:
+    resolution: {integrity: sha512-WsHyJ7b7vzHdJ1fv67Yf++2dz3D726oO3QCu8iNYik4fb5YuuReOI9OtA+n7Mk0xyQivNTPbl181s+5oZ38gyA==}
     engines: {node: '>=12'}
     cpu: [arm]
     os: [linux]
@@ -394,8 +394,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/linux-arm64/0.17.11:
-    resolution: {integrity: sha512-Qxth3gsWWGKz2/qG2d5DsW/57SeA2AmpSMhdg9TSB5Svn2KDob3qxfQSkdnWjSd42kqoxIPy3EJFs+6w1+6Qjg==}
+  /@esbuild/linux-arm64/0.17.12:
+    resolution: {integrity: sha512-cK3AjkEc+8v8YG02hYLQIQlOznW+v9N+OI9BAFuyqkfQFR+DnDLhEM5N8QRxAUz99cJTo1rLNXqRrvY15gbQUg==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [linux]
@@ -403,8 +403,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/linux-ia32/0.17.11:
-    resolution: {integrity: sha512-dB1nGaVWtUlb/rRDHmuDQhfqazWE0LMro/AIbT2lWM3CDMHJNpLckH+gCddQyhhcLac2OYw69ikUMO34JLt3wA==}
+  /@esbuild/linux-ia32/0.17.12:
+    resolution: {integrity: sha512-jdOBXJqcgHlah/nYHnj3Hrnl9l63RjtQ4vn9+bohjQPI2QafASB5MtHAoEv0JQHVb/xYQTFOeuHnNYE1zF7tYw==}
     engines: {node: '>=12'}
     cpu: [ia32]
     os: [linux]
@@ -412,8 +412,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/linux-loong64/0.17.11:
-    resolution: {integrity: sha512-aCWlq70Q7Nc9WDnormntGS1ar6ZFvUpqr8gXtO+HRejRYPweAFQN615PcgaSJkZjhHp61+MNLhzyVALSF2/Q0g==}
+  /@esbuild/linux-loong64/0.17.12:
+    resolution: {integrity: sha512-GTOEtj8h9qPKXCyiBBnHconSCV9LwFyx/gv3Phw0pa25qPYjVuuGZ4Dk14bGCfGX3qKF0+ceeQvwmtI+aYBbVA==}
     engines: {node: '>=12'}
     cpu: [loong64]
     os: [linux]
@@ -421,8 +421,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/linux-mips64el/0.17.11:
-    resolution: {integrity: sha512-cGeGNdQxqY8qJwlYH1BP6rjIIiEcrM05H7k3tR7WxOLmD1ZxRMd6/QIOWMb8mD2s2YJFNRuNQ+wjMhgEL2oCEw==}
+  /@esbuild/linux-mips64el/0.17.12:
+    resolution: {integrity: sha512-o8CIhfBwKcxmEENOH9RwmUejs5jFiNoDw7YgS0EJTF6kgPgcqLFjgoc5kDey5cMHRVCIWc6kK2ShUePOcc7RbA==}
     engines: {node: '>=12'}
     cpu: [mips64el]
     os: [linux]
@@ -430,8 +430,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/linux-ppc64/0.17.11:
-    resolution: {integrity: sha512-BdlziJQPW/bNe0E8eYsHB40mYOluS+jULPCjlWiHzDgr+ZBRXPtgMV1nkLEGdpjrwgmtkZHEGEPaKdS/8faLDA==}
+  /@esbuild/linux-ppc64/0.17.12:
+    resolution: {integrity: sha512-biMLH6NR/GR4z+ap0oJYb877LdBpGac8KfZoEnDiBKd7MD/xt8eaw1SFfYRUeMVx519kVkAOL2GExdFmYnZx3A==}
     engines: {node: '>=12'}
     cpu: [ppc64]
     os: [linux]
@@ -439,8 +439,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/linux-riscv64/0.17.11:
-    resolution: {integrity: sha512-MDLwQbtF+83oJCI1Cixn68Et/ME6gelmhssPebC40RdJaect+IM+l7o/CuG0ZlDs6tZTEIoxUe53H3GmMn8oMA==}
+  /@esbuild/linux-riscv64/0.17.12:
+    resolution: {integrity: sha512-jkphYUiO38wZGeWlfIBMB72auOllNA2sLfiZPGDtOBb1ELN8lmqBrlMiucgL8awBw1zBXN69PmZM6g4yTX84TA==}
     engines: {node: '>=12'}
     cpu: [riscv64]
     os: [linux]
@@ -448,8 +448,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/linux-s390x/0.17.11:
-    resolution: {integrity: sha512-4N5EMESvws0Ozr2J94VoUD8HIRi7X0uvUv4c0wpTHZyZY9qpaaN7THjosdiW56irQ4qnJ6Lsc+i+5zGWnyqWqQ==}
+  /@esbuild/linux-s390x/0.17.12:
+    resolution: {integrity: sha512-j3ucLdeY9HBcvODhCY4b+Ds3hWGO8t+SAidtmWu/ukfLLG/oYDMaA+dnugTVAg5fnUOGNbIYL9TOjhWgQB8W5g==}
     engines: {node: '>=12'}
     cpu: [s390x]
     os: [linux]
@@ -457,8 +457,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/linux-x64/0.17.11:
-    resolution: {integrity: sha512-rM/v8UlluxpytFSmVdbCe1yyKQd/e+FmIJE2oPJvbBo+D0XVWi1y/NQ4iTNx+436WmDHQBjVLrbnAQLQ6U7wlw==}
+  /@esbuild/linux-x64/0.17.12:
+    resolution: {integrity: sha512-uo5JL3cgaEGotaqSaJdRfFNSCUJOIliKLnDGWaVCgIKkHxwhYMm95pfMbWZ9l7GeW9kDg0tSxcy9NYdEtjwwmA==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [linux]
@@ -466,8 +466,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/netbsd-x64/0.17.11:
-    resolution: {integrity: sha512-4WaAhuz5f91h3/g43VBGdto1Q+X7VEZfpcWGtOFXnggEuLvjV+cP6DyLRU15IjiU9fKLLk41OoJfBFN5DhPvag==}
+  /@esbuild/netbsd-x64/0.17.12:
+    resolution: {integrity: sha512-DNdoRg8JX+gGsbqt2gPgkgb00mqOgOO27KnrWZtdABl6yWTST30aibGJ6geBq3WM2TIeW6COs5AScnC7GwtGPg==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [netbsd]
@@ -475,8 +475,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/openbsd-x64/0.17.11:
-    resolution: {integrity: sha512-UBj135Nx4FpnvtE+C8TWGp98oUgBcmNmdYgl5ToKc0mBHxVVqVE7FUS5/ELMImOp205qDAittL6Ezhasc2Ev/w==}
+  /@esbuild/openbsd-x64/0.17.12:
+    resolution: {integrity: sha512-aVsENlr7B64w8I1lhHShND5o8cW6sB9n9MUtLumFlPhG3elhNWtE7M1TFpj3m7lT3sKQUMkGFjTQBrvDDO1YWA==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [openbsd]
@@ -484,8 +484,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/sunos-x64/0.17.11:
-    resolution: {integrity: sha512-1/gxTifDC9aXbV2xOfCbOceh5AlIidUrPsMpivgzo8P8zUtczlq1ncFpeN1ZyQJ9lVs2hILy1PG5KPp+w8QPPg==}
+  /@esbuild/sunos-x64/0.17.12:
+    resolution: {integrity: sha512-qbHGVQdKSwi0JQJuZznS4SyY27tYXYF0mrgthbxXrZI3AHKuRvU+Eqbg/F0rmLDpW/jkIZBlCO1XfHUBMNJ1pg==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [sunos]
@@ -493,8 +493,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/win32-arm64/0.17.11:
-    resolution: {integrity: sha512-vtSfyx5yRdpiOW9yp6Ax0zyNOv9HjOAw8WaZg3dF5djEHKKm3UnoohftVvIJtRh0Ec7Hso0RIdTqZvPXJ7FdvQ==}
+  /@esbuild/win32-arm64/0.17.12:
+    resolution: {integrity: sha512-zsCp8Ql+96xXTVTmm6ffvoTSZSV2B/LzzkUXAY33F/76EajNw1m+jZ9zPfNJlJ3Rh4EzOszNDHsmG/fZOhtqDg==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [win32]
@@ -502,8 +502,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/win32-ia32/0.17.11:
-    resolution: {integrity: sha512-GFPSLEGQr4wHFTiIUJQrnJKZhZjjq4Sphf+mM76nQR6WkQn73vm7IsacmBRPkALfpOCHsopSvLgqdd4iUW2mYw==}
+  /@esbuild/win32-ia32/0.17.12:
+    resolution: {integrity: sha512-FfrFjR4id7wcFYOdqbDfDET3tjxCozUgbqdkOABsSFzoZGFC92UK7mg4JKRc/B3NNEf1s2WHxJ7VfTdVDPN3ng==}
     engines: {node: '>=12'}
     cpu: [ia32]
     os: [win32]
@@ -511,8 +511,8 @@ packages:
     dev: true
     optional: true
 
-  /@esbuild/win32-x64/0.17.11:
-    resolution: {integrity: sha512-N9vXqLP3eRL8BqSy8yn4Y98cZI2pZ8fyuHx6lKjiG2WABpT2l01TXdzq5Ma2ZUBzfB7tx5dXVhge8X9u0S70ZQ==}
+  /@esbuild/win32-x64/0.17.12:
+    resolution: {integrity: sha512-JOOxw49BVZx2/5tW3FqkdjSD/5gXYeVGPDcB0lvap0gLQshkh1Nyel1QazC+wNxus3xPlsYAgqU1BUmrmCvWtw==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [win32]
@@ -1225,7 +1225,7 @@ packages:
       postcss: ^8.1.0
     dependencies:
       browserslist: 4.21.5
-      caniuse-lite: 1.0.30001466
+      caniuse-lite: 1.0.30001468
       fraction.js: 4.2.0
       normalize-range: 0.1.2
       picocolors: 1.0.0
@@ -1302,8 +1302,8 @@ packages:
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
     hasBin: true
     dependencies:
-      caniuse-lite: 1.0.30001466
-      electron-to-chromium: 1.4.332
+      caniuse-lite: 1.0.30001468
+      electron-to-chromium: 1.4.333
       node-releases: 2.0.10
       update-browserslist-db: 1.0.10_browserslist@4.21.5
     dev: true
@@ -1377,13 +1377,13 @@ packages:
     resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
     dependencies:
       browserslist: 4.21.5
-      caniuse-lite: 1.0.30001466
+      caniuse-lite: 1.0.30001468
       lodash.memoize: 4.1.2
       lodash.uniq: 4.5.0
     dev: true
 
-  /caniuse-lite/1.0.30001466:
-    resolution: {integrity: sha512-ewtFBSfWjEmxUgNBSZItFSmVtvk9zkwkl1OfRZlKA8slltRN+/C/tuGVrF9styXkN36Yu3+SeJ1qkXxDEyNZ5w==}
+  /caniuse-lite/1.0.30001468:
+    resolution: {integrity: sha512-zgAo8D5kbOyUcRAgSmgyuvBkjrGk5CGYG5TYgFdpQv+ywcyEpo1LOWoG8YmoflGnh+V+UsNuKYedsoYs0hzV5A==}
     dev: true
 
   /catharsis/0.9.0:
@@ -1604,7 +1604,7 @@ packages:
       execa: 4.1.0
       polyfill-library: 3.111.0
       semver: 7.3.8
-      snyk: 1.1119.0
+      snyk: 1.1121.0
       yargs: 15.4.1
     transitivePeerDependencies:
       - supports-color
@@ -1870,8 +1870,8 @@ packages:
       tslib: 2.5.0
     dev: true
 
-  /electron-to-chromium/1.4.332:
-    resolution: {integrity: sha512-c1Vbv5tuUlBFp0mb3mCIjw+REEsgthRgNE8BlbEDKmvzb8rxjcVki6OkQP83vLN34s0XCxpSkq7AZNep1a6xhw==}
+  /electron-to-chromium/1.4.333:
+    resolution: {integrity: sha512-YyE8+GKyGtPEP1/kpvqsdhD6rA/TP1DUFDN4uiU/YI52NzDxmwHkEb3qjId8hLBa5siJvG0sfC3O66501jMruQ==}
     dev: true
 
   /emoji-regex/7.0.3:
@@ -1905,34 +1905,34 @@ packages:
     resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==}
     dev: true
 
-  /esbuild/0.17.11:
-    resolution: {integrity: sha512-pAMImyokbWDtnA/ufPxjQg0fYo2DDuzAlqwnDvbXqHLphe+m80eF++perYKVm8LeTuj2zUuFXC+xgSVxyoHUdg==}
+  /esbuild/0.17.12:
+    resolution: {integrity: sha512-bX/zHl7Gn2CpQwcMtRogTTBf9l1nl+H6R8nUbjk+RuKqAE3+8FDulLA+pHvX7aA7Xe07Iwa+CWvy9I8Y2qqPKQ==}
     engines: {node: '>=12'}
     hasBin: true
     requiresBuild: true
     optionalDependencies:
-      '@esbuild/android-arm': 0.17.11
-      '@esbuild/android-arm64': 0.17.11
-      '@esbuild/android-x64': 0.17.11
-      '@esbuild/darwin-arm64': 0.17.11
-      '@esbuild/darwin-x64': 0.17.11
-      '@esbuild/freebsd-arm64': 0.17.11
-      '@esbuild/freebsd-x64': 0.17.11
-      '@esbuild/linux-arm': 0.17.11
-      '@esbuild/linux-arm64': 0.17.11
-      '@esbuild/linux-ia32': 0.17.11
-      '@esbuild/linux-loong64': 0.17.11
-      '@esbuild/linux-mips64el': 0.17.11
-      '@esbuild/linux-ppc64': 0.17.11
-      '@esbuild/linux-riscv64': 0.17.11
-      '@esbuild/linux-s390x': 0.17.11
-      '@esbuild/linux-x64': 0.17.11
-      '@esbuild/netbsd-x64': 0.17.11
-      '@esbuild/openbsd-x64': 0.17.11
-      '@esbuild/sunos-x64': 0.17.11
-      '@esbuild/win32-arm64': 0.17.11
-      '@esbuild/win32-ia32': 0.17.11
-      '@esbuild/win32-x64': 0.17.11
+      '@esbuild/android-arm': 0.17.12
+      '@esbuild/android-arm64': 0.17.12
+      '@esbuild/android-x64': 0.17.12
+      '@esbuild/darwin-arm64': 0.17.12
+      '@esbuild/darwin-x64': 0.17.12
+      '@esbuild/freebsd-arm64': 0.17.12
+      '@esbuild/freebsd-x64': 0.17.12
+      '@esbuild/linux-arm': 0.17.12
+      '@esbuild/linux-arm64': 0.17.12
+      '@esbuild/linux-ia32': 0.17.12
+      '@esbuild/linux-loong64': 0.17.12
+      '@esbuild/linux-mips64el': 0.17.12
+      '@esbuild/linux-ppc64': 0.17.12
+      '@esbuild/linux-riscv64': 0.17.12
+      '@esbuild/linux-s390x': 0.17.12
+      '@esbuild/linux-x64': 0.17.12
+      '@esbuild/netbsd-x64': 0.17.12
+      '@esbuild/openbsd-x64': 0.17.12
+      '@esbuild/sunos-x64': 0.17.12
+      '@esbuild/win32-arm64': 0.17.12
+      '@esbuild/win32-ia32': 0.17.12
+      '@esbuild/win32-x64': 0.17.12
     dev: true
 
   /escalade/3.1.1:
@@ -4095,8 +4095,8 @@ packages:
       supports-color: 7.2.0
     dev: true
 
-  /snyk/1.1119.0:
-    resolution: {integrity: sha512-kcc3TmAwpA1p4gz7Myf1DcoM6e1FWc/1iBuAEPOzXaGmuy6j8lvy6g00Fj/fHKK+ZRBkdqun6lBxEqfSM9utsQ==}
+  /snyk/1.1121.0:
+    resolution: {integrity: sha512-0uGoG/8xOWopWB5OdJYGDx2h1JcNbWCDS19CHy59QfDuQNXcAPXUffxCfmyDD8KzDeCv9HfLF66lNcCqSjr+gA==}
     engines: {node: '>=12'}
     hasBin: true
     requiresBuild: true
@@ -4610,7 +4610,7 @@ packages:
       terser:
         optional: true
     dependencies:
-      esbuild: 0.17.11
+      esbuild: 0.17.12
       postcss: 8.4.21
       resolve: 1.22.1
       rollup: 3.19.1
@@ -4644,7 +4644,7 @@ packages:
         optional: true
     dependencies:
       '@types/node': 18.15.3
-      esbuild: 0.17.11
+      esbuild: 0.17.12
       postcss: 8.4.21
       resolve: 1.22.1
       rollup: 3.19.1
diff --git a/development/test/cases/text/formatter.mjs b/development/test/cases/text/formatter.mjs
index 874bb328ce4e2f768d6206dc1428dc4fd68f6192..6a00fa2fc777c30975b492ae26ec5a2f6cefd47f 100644
--- a/development/test/cases/text/formatter.mjs
+++ b/development/test/cases/text/formatter.mjs
@@ -204,4 +204,84 @@ describe('Formatter', function () {
     });
 
 
+
+
+    describe('Formatter', () => {
+        it('should format a basic string with object values', () => {
+            const formatter = new Formatter({name: 'John', age: 30});
+            const result = formatter.format('My name is ${name} and I am ${age | tostring} years old.');
+
+            expect(result).to.equal('My name is John and I am 30 years old.');
+        });
+
+        it('should format a string with nested markers', () => {
+            const text = '${mykey${subkey}}';
+            const obj = {mykey2: '1', subkey: '2'};
+            const formatter = new Formatter(obj);
+
+            expect(formatter.format(text)).to.equal('1');
+        });
+
+        it('should format a string with custom markers', () => {
+            const formatter = new Formatter({name: 'John', age: 30});
+            formatter.setMarker('[', ']');
+            const result = formatter.format('My name is [name] and I am [age | tostring] years old.');
+
+            expect(result).to.equal('My name is John and I am 30 years old.');
+        });
+
+        it('should format a string using callback', () => {
+            const formatter = new Formatter({x: '1'}, {
+                callbacks: {
+                    quote: (value) => {
+                        return '"' + value + '"';
+                    },
+                },
+            });
+
+            expect(formatter.format('${x | call:quote}')).to.equal('"1"');
+        });
+
+        it('should format a string with parameters', () => {
+            const obj = {
+                a: {
+                    b: {
+                        c: 'Hello',
+                    },
+                    d: 'world',
+                },
+            };
+            const formatter = new Formatter(obj);
+            const result = formatter.format('${a.b.c} ${a.d | ucfirst}!');
+
+            expect(result).to.equal('Hello World!');
+        });
+        
+        it('should throw a too deep nesting error', () => {
+            const formatter = new Formatter({name: 'John'});
+            const nestedText = '${name${name${name${name${name${name${name${name${name${name${name${name${name${name${name${name${name${name${name}}}}}}}}}}}}}}}}}}';
+            expect(() => formatter.format(nestedText)).to.throw('syntax error in formatter template');
+        });
+
+        it('should throw a too deep nesting error', () => {
+            const inputObj = {
+                mykey: '${mykey}',
+            };
+
+            const formatter = new Formatter(inputObj);
+
+            const text = '${mykey}';
+            let formattedText = text;
+
+            // Create a string with 21 levels of nesting
+            for (let i = 0; i < 21; i++) {
+                formattedText = '${' + formattedText + '}';
+            }
+
+            expect(() => formatter.format(formattedText)).to.throw('too deep nesting');
+        });
+        
+    });
+
+
 });
\ No newline at end of file
diff --git a/development/test/cases/text/util.mjs b/development/test/cases/text/util.mjs
new file mode 100644
index 0000000000000000000000000000000000000000..4cef64c1fdc020c48a359c924affb1a8e15038dd
--- /dev/null
+++ b/development/test/cases/text/util.mjs
@@ -0,0 +1,109 @@
+import {expect} from "chai"
+import {generateRangeComparisonExpression} from "../../../../application/source/text/util.mjs";
+
+describe('generateRangeComparisonExpression', () => {
+    it('should generate correct comparison expression for single values', () => {
+        const expression = '1,3,5';
+        const valueName = 'x';
+        const result = generateRangeComparisonExpression(expression, valueName);
+        expect(result).to.equal('(x==1) || (x==3) || (x==5)');
+    });
+
+    it('should generate correct comparison expression for ranges', () => {
+        const expression = '1-3,6-8';
+        const valueName = 'x';
+        const result = generateRangeComparisonExpression(expression, valueName);
+        expect(result).to.equal('(x>=1 && x<=3) || (x>=6 && x<=8)');
+    });
+
+    it('should generate correct comparison expression for mixed ranges and single values', () => {
+        const expression = '1-3,5,7-9';
+        const valueName = 'x';
+        const result = generateRangeComparisonExpression(expression, valueName);
+        expect(result).to.equal('(x>=1 && x<=3) || (x==5) || (x>=7 && x<=9)');
+    });
+
+    it('should throw an error for invalid range', () => {
+        const expression = '1-3,5-4';
+        const valueName = 'x';
+        expect(() => generateRangeComparisonExpression(expression, valueName)).to.throw(`Invalid range '5-4'`);
+    });
+
+
+    it('should throw an error for invalid value', () => {
+        const expression = '1-3,a';
+        const valueName = 'x';
+        expect(() => generateRangeComparisonExpression(expression, valueName)).to.throw('Invalid value');
+    });
+
+    it('should generate correct comparison expression with custom operators', () => {
+        const expression = '1-3,5';
+        const valueName = 'x';
+        const options = {
+            andOp: 'AND',
+            orOp: 'OR',
+            eqOp: '===',
+            geOp: '>=',
+            leOp: '<=',
+        };
+        const result = generateRangeComparisonExpression(expression, valueName, options);
+        expect(result).to.equal('(x>=1 AND x<=3) OR (x===5)');
+    });
+
+    it('should generate correct comparison expression with urlEncode option', () => {
+        const testCases = [
+            {
+                expression: '1,3,5',
+                valueName: 'x',
+                expected: '(x%3D%3D1) || (x%3D%3D3) || (x%3D%3D5)',
+            },
+            {
+                expression: '-10',
+                valueName: 'x',
+                expected: 'x%3C%3D10',
+            },
+            {
+                expression: '10-',
+                valueName: 'x',
+                expected: 'x%3E%3D10',
+            },
+            {
+                expression: '1-3,6-8',
+                valueName: 'y',
+                expected: '(y%3E%3D1 && y%3C%3D3) || (y%3E%3D6 && y%3C%3D8)',
+            },
+            {
+                expression: '1-3,5,7-9',
+                valueName: 'z',
+                expected: '(z%3E%3D1 && z%3C%3D3) || (z%3D%3D5) || (z%3E%3D7 && z%3C%3D9)',
+            },
+        ];
+
+        testCases.forEach(({expression, valueName, expected}) => {
+            const result = generateRangeComparisonExpression(expression, valueName, {urlEncode: true});
+            expect(result).to.equal(expected);
+        });
+    });
+
+    it('should generate correct comparison expression for open-ended ranges with urlEncode option', () => {
+        const testCases = [
+            {
+                expression: '10-',
+                valueName: 'x',
+                expected: 'x%3E%3D10',
+            },
+            {
+                expression: '-10',
+                valueName: 'y',
+                expected: 'y%3C%3D10',
+            },
+        ];
+
+        testCases.forEach(({expression, valueName, expected}) => {
+            const result = generateRangeComparisonExpression(expression, valueName, {urlEncode: true});
+            expect(result).to.equal(expected);
+        });
+    });
+
+
+});