From ebf28ffb69ec9af6e93dbc7bb4113095a5f3d4f8 Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Sun, 2 Apr 2023 17:11:39 +0200
Subject: [PATCH] feat: new functions parseBracketedKeyValueHash and
 createBracketedKeyValueHash

---
 .../source/text/bracketed-key-value-hash.mjs  | 245 ++++++++++++++++++
 .../generate-range-comparison-expression.mjs  |  93 +++++++
 application/source/text/util.mjs              |  86 +-----
 .../cases/text/bracketed-key-value-hash.mjs   | 214 +++++++++++++++
 4 files changed, 553 insertions(+), 85 deletions(-)
 create mode 100644 application/source/text/bracketed-key-value-hash.mjs
 create mode 100644 application/source/text/generate-range-comparison-expression.mjs
 create mode 100644 development/test/cases/text/bracketed-key-value-hash.mjs

diff --git a/application/source/text/bracketed-key-value-hash.mjs b/application/source/text/bracketed-key-value-hash.mjs
new file mode 100644
index 000000000..dc5f39b6f
--- /dev/null
+++ b/application/source/text/bracketed-key-value-hash.mjs
@@ -0,0 +1,245 @@
+/**
+ * Copyright schukai GmbH and contributors 2023. All Rights Reserved.
+ * Node module: @schukai/monster
+ * This file is licensed under the AGPLv3 License.
+ * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
+ */
+
+export {parseBracketedKeyValueHash, createBracketedKeyValueHash}
+
+/**
+ * Parses a string containing bracketed key-value pairs and returns an object representing the parsed result.
+ *
+ * - The string starts with a hash symbol #.
+ * - After the hash symbol, there are one or more selector strings, separated by a semicolon ;.
+ * - Each selector string has the format selectorName(key1=value1,key2=value2,...).
+ * - The selector name is a string of one or more alphanumeric characters.
+ * - The key-value pairs are separated by commas , and are of the form key=value.
+ * - The key is a string of one or more alphanumeric characters.
+ * - The value can be an empty string or a string of one or more characters.
+ * - If the value contains commas, it must be enclosed in double quotes ".
+ * - The entire key-value pair must be URL-encoded.
+ * - The closing parenthesis ) for each selector must be present, even if there are no key-value pairs.
+ *
+ * @example
+ *
+ * ```javascript
+ * // Example 1:
+ * const hashString = '#selector1(key1=value1,key2=value2);selector2(key3=value3)';
+ * const result = parseBracketedKeyValueHash(hashString);
+ * // result => { selector1: { key1: "value1", key2: "value2" }, selector2: { key3: "value3" } }
+ * ```
+ *
+ * @example
+ *
+ * ```javascript
+ * // Example 2:
+ * const hashString = '#selector1(key1=value1,key2=value2);selector2(';
+ * const result = parseBracketedKeyValueHash(hashString);
+ * // result => {}
+ * ```
+ *
+ * @since 3.37.0
+ * @param {string} hashString - The string to parse, containing bracketed key-value pairs.
+ * @returns {Object} - An object representing the parsed result, with keys representing the selectors and values representing the key-value pairs associated with each selector.
+ *                    - Returns an empty object if there was an error during parsing. */
+function parseBracketedKeyValueHash(hashString) {
+    const selectors = {};
+    //const selectorStack = [];
+    //const keyValueStack = [];
+
+    const trimmedHashString = hashString.trim();
+    const cleanedHashString = trimmedHashString.charAt(0) === '#' ? trimmedHashString.slice(1) : trimmedHashString;
+
+
+    //const selectors = (keyValueStack.length > 0) ? result[selectorStack[selectorStack.length - 1]] : result;
+    let currentSelector = "";
+
+    function addToResult(key, value) {
+        if (currentSelector && key) {
+            if (!selectors[currentSelector]) {
+                selectors[currentSelector] = {};
+            }
+
+            selectors[currentSelector][key] = value;
+        }
+    }
+
+    let currentKey = '';
+    let currentValue = '';
+    let inKey = true;
+    let inValue = false;
+    let inQuotedValue = false;
+    let inSelector = true;
+    let escaped = false;
+    let quotedValueStartChar = '';
+
+    for (let i = 0; i < cleanedHashString.length; i++) {
+        const c = cleanedHashString[i];
+        const nextChar = cleanedHashString?.[i + 1];
+
+        if (c === '\\' && !escaped) {
+            escaped = true;
+            continue;
+        }
+
+        if (escaped) {
+            if (inSelector) {
+                currentSelector += c;
+            } else if (inKey) {
+                currentKey += c;
+            } else if (inValue) {
+                currentValue += c;
+            }
+            escaped = false;
+            continue;
+        }
+
+        if (inQuotedValue && quotedValueStartChar !== c) {
+
+            if (inSelector) {
+                currentSelector += c;
+            } else if (inKey) {
+                currentKey += c;
+            } else if (inValue) {
+                currentValue += c;
+            }
+
+            continue;
+        }
+
+        if (c === ';' && inSelector) {
+            inSelector = true;
+            currentSelector = "";
+            continue;
+        }
+
+
+        if (inSelector === true && c !== '(') {
+            currentSelector += c;
+            continue;
+        }
+
+        if (c === '(' && inSelector) {
+            inSelector = false;
+            inKey = true;
+
+            currentKey = "";
+            continue;
+        }
+
+        if (inKey === true && c !== '=') {
+            currentKey += c;
+            continue;
+        }
+
+        if (c === '=' && inKey) {
+
+            inKey = false;
+            inValue = true;
+
+            if (nextChar === '"' || nextChar === "'") {
+                inQuotedValue = true;
+                quotedValueStartChar = nextChar;
+                i++;
+                continue;
+            }
+
+            currentValue = "";
+            continue;
+        }
+
+        if (inValue === true) {
+            if (inQuotedValue) {
+                if (c === quotedValueStartChar) {
+                    inQuotedValue = false;
+                    continue;
+                }
+
+                currentValue += c;
+                continue;
+            }
+
+            if (c === ',') {
+                inValue = false;
+                inKey = true;
+                const decodedCurrentValue = decodeURIComponent(currentValue);
+                addToResult(currentKey, decodedCurrentValue);
+                currentKey = "";
+                currentValue = "";
+                continue;
+            }
+
+            if (c === ')') {
+                inValue = false;
+                //inKey = true;
+                inSelector = true;
+
+                const decodedCurrentValue = decodeURIComponent(currentValue);
+                addToResult(currentKey, decodedCurrentValue);
+                currentKey = "";
+                currentValue = "";
+                currentSelector = "";
+                continue;
+            }
+
+            currentValue += c;
+
+            continue;
+        }
+    }
+
+
+    if (inSelector) {
+        return selectors;
+    }
+
+
+    return {};
+
+}
+
+/**
+ * Creates a hash selector string from an object.
+ *
+ * @param {Object} object - The object containing selectors and key-value pairs.
+ * @param {boolean} addHashPrefix - Whether to add the hash prefix # to the beginning of the string.
+ * @returns {string} The hash selector string.
+ * @since 3.37.0
+ */
+function createBracketedKeyValueHash(object, addHashPrefix = true) {
+    
+    if (!object) {
+        return addHashPrefix ? '#' : '';
+    }
+    
+    let hashString = '';
+
+    function encodeKeyValue(key, value) {
+        return encodeURIComponent(key) + '=' + encodeURIComponent(value);
+    }
+
+    for (const selector in object) {
+        if (object.hasOwnProperty(selector)) {
+            const keyValuePairs = object[selector];
+            let selectorString = selector;
+            let keyValueString = '';
+
+            for (const key in keyValuePairs) {
+                if (keyValuePairs.hasOwnProperty(key)) {
+                    const value = keyValuePairs[key];
+                    keyValueString += keyValueString.length === 0 ? '' : ',';
+                    keyValueString += encodeKeyValue(key, value);
+                }
+            }
+
+            if (keyValueString.length > 0) {
+                selectorString += '(' + keyValueString + ')';
+                hashString += hashString.length === 0 ? '' : ';';
+                hashString += selectorString;
+            }
+        }
+    }
+
+    return addHashPrefix ? '#' + hashString : hashString;
+}
\ No newline at end of file
diff --git a/application/source/text/generate-range-comparison-expression.mjs b/application/source/text/generate-range-comparison-expression.mjs
new file mode 100644
index 000000000..367516e3c
--- /dev/null
+++ b/application/source/text/generate-range-comparison-expression.mjs
@@ -0,0 +1,93 @@
+/**
+ * Copyright schukai GmbH and contributors 2023. All Rights Reserved.
+ * Node module: @schukai/monster
+ * This file is licensed under the AGPLv3 License.
+ * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
+ */
+
+export { generateRangeComparisonExpression };
+
+/**
+ * The `generateRangeComparisonExpression()` function is function that generates a string representation
+ * of a comparison expression based on a range of values. It takes three arguments:
+ *
+ * - expression (required): a string representation of a range of values in the format of start1-end1,start2-end2,value3....
+ * - valueName (required): a string representing the name of the value that is being compared to the range of values.
+ * - options (optional): an object containing additional options to customize the comparison expression.
+ *
+ * The generateRangeComparisonExpression() function returns a string representation of the comparison expression.
+ *
+ * ## Options
+ * The options parameter is an object that can have the following properties:
+ *
+ * urlEncode (boolean, default: false): if set to true, URL encodes the comparison operators.
+ * andOp (string, default: '&&'): the logical AND operator to use in the expression.
+ * orOp (string, default: '||'): the logical OR operator to use in the expression.
+ * eqOp (string, default: '=='): the equality operator to use in the expression.
+ * geOp (string, default: '>='): the greater than or equal to operator to use in the expression.
+ * leOp (string, default: '<='): the less than or equal to operator to use in the expression.
+ *
+ * Examples
+ *
+ * ```javascript
+ * const expression = '0-10,20-30';
+ * const valueName = 'age';
+ * const options = { urlEncode: true, andOp: 'and', orOp: 'or', eqOp: '=', geOp: '>=', leOp: '<=' };
+ * const comparisonExpression = generateRangeComparisonExpression(expression, valueName, options);
+ *
+ * console.log(comparisonExpression); // age%3E%3D0%20and%20age%3C%3D10%20or%20age%3E%3D20%20and%20age%3C%3D30
+ * ```
+ *
+ * In this example, the generateRangeComparisonExpression() function generates a string representation of the comparison
+ * expression for the expression and valueName parameters with the specified options. The resulting comparison
+ * expression is 'age>=0 and age<=10 or age>=20 and age<=30', URL encoded according to the urlEncode option.
+ *
+ * @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.
+ * @memberOf Monster.Text
+ * @summary Generates a comparison expression based on a range of values.
+ */
+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/application/source/text/util.mjs b/application/source/text/util.mjs
index 64704efcc..29f2d90fb 100644
--- a/application/source/text/util.mjs
+++ b/application/source/text/util.mjs
@@ -1,86 +1,2 @@
-export { generateRangeComparisonExpression };
+export { generateRangeComparisonExpression } from "./generate-range-comparison-expression.mjs"
 
-/**
- * The `generateRangeComparisonExpression()` function is function that generates a string representation
- * of a comparison expression based on a range of values. It takes three arguments:
- *
- * - expression (required): a string representation of a range of values in the format of start1-end1,start2-end2,value3....
- * - valueName (required): a string representing the name of the value that is being compared to the range of values.
- * - options (optional): an object containing additional options to customize the comparison expression.
- *
- * The generateRangeComparisonExpression() function returns a string representation of the comparison expression.
- *
- * ## Options
- * The options parameter is an object that can have the following properties:
- *
- * urlEncode (boolean, default: false): if set to true, URL encodes the comparison operators.
- * andOp (string, default: '&&'): the logical AND operator to use in the expression.
- * orOp (string, default: '||'): the logical OR operator to use in the expression.
- * eqOp (string, default: '=='): the equality operator to use in the expression.
- * geOp (string, default: '>='): the greater than or equal to operator to use in the expression.
- * leOp (string, default: '<='): the less than or equal to operator to use in the expression.
- *
- * Examples
- *
- * ```javascript
- * const expression = '0-10,20-30';
- * const valueName = 'age';
- * const options = { urlEncode: true, andOp: 'and', orOp: 'or', eqOp: '=', geOp: '>=', leOp: '<=' };
- * const comparisonExpression = generateRangeComparisonExpression(expression, valueName, options);
- *
- * console.log(comparisonExpression); // age%3E%3D0%20and%20age%3C%3D10%20or%20age%3E%3D20%20and%20age%3C%3D30
- * ```
- *
- * In this example, the generateRangeComparisonExpression() function generates a string representation of the comparison
- * expression for the expression and valueName parameters with the specified options. The resulting comparison
- * expression is 'age>=0 and age<=10 or age>=20 and age<=30', URL encoded according to the urlEncode option.
- *
- * @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.
- * @memberOf Monster.Text
- * @summary Generates a comparison expression based on a range of values.
- */
-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/test/cases/text/bracketed-key-value-hash.mjs b/development/test/cases/text/bracketed-key-value-hash.mjs
new file mode 100644
index 000000000..a2bc319c1
--- /dev/null
+++ b/development/test/cases/text/bracketed-key-value-hash.mjs
@@ -0,0 +1,214 @@
+// test.js
+import {expect} from "chai";
+import {
+    parseBracketedKeyValueHash,
+    createBracketedKeyValueHash
+} from "../../../../application/source/text/bracketed-key-value-hash.mjs";
+
+describe("parseBracketedKeyValueHash", () => {
+    it("should return an empty object for an empty string", () => {
+        const input = "";
+        const expectedResult = {};
+        expect(parseBracketedKeyValueHash(input)).to.deep.equal(expectedResult);
+    });
+
+    it("should parse a single selector with one key-value pair", () => {
+        const input = "#selector1(key1=value1)";
+        const expectedResult = {
+            selector1: {
+                key1: "value1",
+            },
+        };
+        expect(parseBracketedKeyValueHash(input)).to.deep.equal(expectedResult);
+    });
+
+    it("should parse multiple selectors with multiple key-value pairs", () => {
+        const input = "#selector1(key1=value1,key2=value2);selector2(key3=value3,key4=value4)";
+        const expectedResult = {
+            selector1: {
+                key1: "value1",
+                key2: "value2",
+            },
+            selector2: {
+                key3: "value3",
+                key4: "value4",
+            },
+        };
+        expect(parseBracketedKeyValueHash(input)).to.deep.equal(expectedResult);
+    });
+
+    it("should decode URL-encoded values", () => {
+        const input = "#selector1(key1=value1%2Cwith%20comma)";
+        const expectedResult = {
+            selector1: {
+                key1: "value1,with comma",
+            },
+        };
+        const result = parseBracketedKeyValueHash(input);
+        expect(result.selector1.key1).to.equal(expectedResult.selector1.key1);
+    });
+
+    it("should handle input without a leading hash", () => {
+        const input = "selector1(key1=value1)";
+        const expectedResult = {
+            selector1: {
+                key1: "value1",
+            },
+        };
+        expect(parseBracketedKeyValueHash(input)).to.deep.equal(expectedResult);
+    });
+
+    it("should return an empty object for invalid input", () => {
+        const input = "#selector1(key1=value1,key2";
+        const expectedResult = {};
+        expect(parseBracketedKeyValueHash(input)).to.deep.equal(expectedResult);
+    });
+
+    it('should return an empty object for an empty input string', () => {
+        const hashString = '';
+        const result = parseBracketedKeyValueHash(hashString);
+        expect(result).to.deep.equal({});
+    });
+
+    it('should return an empty object for an invalid input string', () => {
+        const hashString = '#invalid';
+        const result = parseBracketedKeyValueHash(hashString);
+        expect(result).to.deep.equal({});
+    });
+
+    it('should parse a simple input string with one selector and one key-value pair', () => {
+        const hashString = '#selector(key=value)';
+        const result = parseBracketedKeyValueHash(hashString);
+        expect(result).to.deep.equal({selector: {key: 'value'}});
+    });
+
+    it('should parse an input string with multiple selectors and key-value pairs', () => {
+        const hashString = '#selector1(key1=value1);selector2(key2=value2)';
+        const result = parseBracketedKeyValueHash(hashString);
+        expect(result).to.deep.equal({selector1: {key1: 'value1'}, selector2: {key2: 'value2'}});
+    });
+
+    it('should handle empty values', () => {
+        const hashString = '#selector(key1=,key2=)';
+        const result = parseBracketedKeyValueHash(hashString);
+        expect(result).to.deep.equal({selector: {key1: '', key2: ''}});
+    });
+
+    it('should handle percent-encoded values', () => {
+        const hashString = '#selector(key1=value%201,key2=value%2C2)';
+        const result = parseBracketedKeyValueHash(hashString);
+        expect(result).to.deep.equal({selector: {key1: 'value 1', key2: 'value,2'}});
+    });
+
+    it('should handle double-quoted values with commas', () => {
+        const hashString = '#selector(key1="value,1",key2="value,2")';
+        const result = parseBracketedKeyValueHash(hashString);
+        expect(result).to.deep.equal({selector: {key1: 'value,1', key2: 'value,2'}});
+    });
+
+    it('should ignore leading hash symbol (#)', () => {
+        const hashString = 'selector(key=value)';
+        const result = parseBracketedKeyValueHash(hashString);
+        expect(result).to.deep.equal({selector: {key: 'value'}});
+    });
+
+    it('should ignore leading and trailing white space', () => {
+        const hashString = '  #selector(key=value)  ';
+        const result = parseBracketedKeyValueHash(hashString);
+        expect(result).to.deep.equal({selector: {key: 'value'}});
+    });
+
+    it('should return an empty object if the input string ends prematurely', () => {
+        const hashString = '#selector(key=value';
+        const result = parseBracketedKeyValueHash(hashString);
+        expect(result).to.deep.equal({});
+    });
+
+    it('should return an empty object if a selector is missing', () => {
+        const hashString = '#(key=value)';
+        const result = parseBracketedKeyValueHash(hashString);
+        expect(result).to.deep.equal({});
+    });
+
+    it('should return an empty object if a key is missing', () => {
+        const hashString = '#selector(=value)';
+        const result = parseBracketedKeyValueHash(hashString);
+        expect(result).to.deep.equal({});
+    });
+
+    it('should return an empty object ifa value is missing', () => {
+        const hashString = '#selector(key=)';
+        const result = parseBracketedKeyValueHash(hashString);
+        expect(result).to.deep.equal({
+            selector: {
+                key: '',
+            },
+        });
+    });
+
+    it('should return an empty object if there is no closing parenthesis for a selector', () => {
+        const hashString = '#selector(key=value;';
+        const result = parseBracketedKeyValueHash(hashString);
+        expect(result).to.deep.equal({});
+    });
+
+    it('should return an empty object if there is no semicolon after a selector', () => {
+        const hashString = '#selector(key=value)selector2(key2=value2)';
+        const result = parseBracketedKeyValueHash(hashString);
+        expect(result).to.deep.equal({
+            selector: {
+                key: 'value',
+            },
+            selector2: {
+                key2: 'value2',
+            },
+        });
+    });
+
+    describe('createBracketedKeyValueHash', () => {
+        it('should return an hash string for a simple object', () => {
+            const input = {
+                '.example': {
+                    'color': 'red',
+                    'font-size': '14px'
+                },
+                '.other': {
+                    'background': 'blue'
+                }
+            };
+
+            const result = createBracketedKeyValueHash(input);
+            expect(result).to.deep.equal("#.example(color=red,font-size=14px);.other(background=blue)");
+        });
+        
+        it('should return a url-encoded hash string for a simple object', () => {
+            const input = {
+                '.example': {
+                    'color': 'r"ed',
+                    'font-size': '14px'
+                },
+                '.other': {
+                    'background': 'blue'
+                }
+            };
+
+            const result = createBracketedKeyValueHash(input, true);
+            expect(result).to.deep.equal("#.example(color=r%22ed,font-size=14px);.other(background=blue)");
+        });
+        
+        it('should return an empty string for an empty object', () => {
+            const input = {};
+            const result = createBracketedKeyValueHash(input,false);
+            expect(result).to.deep.equal("");
+        });
+        
+        it('should return an empty string for an empty object', () => {
+            const input = {};
+            const result = createBracketedKeyValueHash(input,false);
+            expect(result).to.deep.equal("");
+        });
+        
+    });
+
+
+});
-- 
GitLab