@schukai/monster 3.36.0 → 3.37.0

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schukai/monster",
3
- "version": "3.36.0",
3
+ "version": "3.37.0",
4
4
  "description": "Monster is a simple library for creating fast, robust and lightweight websites.",
5
5
  "keywords": [
6
6
  "framework",
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Copyright schukai GmbH and contributors 2023. All Rights Reserved.
3
+ * Node module: @schukai/monster
4
+ * This file is licensed under the AGPLv3 License.
5
+ * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
6
+ */
7
+
8
+ export {parseBracketedKeyValueHash, createBracketedKeyValueHash}
9
+
10
+ /**
11
+ * Parses a string containing bracketed key-value pairs and returns an object representing the parsed result.
12
+ *
13
+ * - The string starts with a hash symbol #.
14
+ * - After the hash symbol, there are one or more selector strings, separated by a semicolon ;.
15
+ * - Each selector string has the format selectorName(key1=value1,key2=value2,...).
16
+ * - The selector name is a string of one or more alphanumeric characters.
17
+ * - The key-value pairs are separated by commas , and are of the form key=value.
18
+ * - The key is a string of one or more alphanumeric characters.
19
+ * - The value can be an empty string or a string of one or more characters.
20
+ * - If the value contains commas, it must be enclosed in double quotes ".
21
+ * - The entire key-value pair must be URL-encoded.
22
+ * - The closing parenthesis ) for each selector must be present, even if there are no key-value pairs.
23
+ *
24
+ * @example
25
+ *
26
+ * ```javascript
27
+ * // Example 1:
28
+ * const hashString = '#selector1(key1=value1,key2=value2);selector2(key3=value3)';
29
+ * const result = parseBracketedKeyValueHash(hashString);
30
+ * // result => { selector1: { key1: "value1", key2: "value2" }, selector2: { key3: "value3" } }
31
+ * ```
32
+ *
33
+ * @example
34
+ *
35
+ * ```javascript
36
+ * // Example 2:
37
+ * const hashString = '#selector1(key1=value1,key2=value2);selector2(';
38
+ * const result = parseBracketedKeyValueHash(hashString);
39
+ * // result => {}
40
+ * ```
41
+ *
42
+ * @since 3.37.0
43
+ * @param {string} hashString - The string to parse, containing bracketed key-value pairs.
44
+ * @returns {Object} - An object representing the parsed result, with keys representing the selectors and values representing the key-value pairs associated with each selector.
45
+ * - Returns an empty object if there was an error during parsing. */
46
+ function parseBracketedKeyValueHash(hashString) {
47
+ const selectors = {};
48
+ //const selectorStack = [];
49
+ //const keyValueStack = [];
50
+
51
+ const trimmedHashString = hashString.trim();
52
+ const cleanedHashString = trimmedHashString.charAt(0) === '#' ? trimmedHashString.slice(1) : trimmedHashString;
53
+
54
+
55
+ //const selectors = (keyValueStack.length > 0) ? result[selectorStack[selectorStack.length - 1]] : result;
56
+ let currentSelector = "";
57
+
58
+ function addToResult(key, value) {
59
+ if (currentSelector && key) {
60
+ if (!selectors[currentSelector]) {
61
+ selectors[currentSelector] = {};
62
+ }
63
+
64
+ selectors[currentSelector][key] = value;
65
+ }
66
+ }
67
+
68
+ let currentKey = '';
69
+ let currentValue = '';
70
+ let inKey = true;
71
+ let inValue = false;
72
+ let inQuotedValue = false;
73
+ let inSelector = true;
74
+ let escaped = false;
75
+ let quotedValueStartChar = '';
76
+
77
+ for (let i = 0; i < cleanedHashString.length; i++) {
78
+ const c = cleanedHashString[i];
79
+ const nextChar = cleanedHashString?.[i + 1];
80
+
81
+ if (c === '\\' && !escaped) {
82
+ escaped = true;
83
+ continue;
84
+ }
85
+
86
+ if (escaped) {
87
+ if (inSelector) {
88
+ currentSelector += c;
89
+ } else if (inKey) {
90
+ currentKey += c;
91
+ } else if (inValue) {
92
+ currentValue += c;
93
+ }
94
+ escaped = false;
95
+ continue;
96
+ }
97
+
98
+ if (inQuotedValue && quotedValueStartChar !== c) {
99
+
100
+ if (inSelector) {
101
+ currentSelector += c;
102
+ } else if (inKey) {
103
+ currentKey += c;
104
+ } else if (inValue) {
105
+ currentValue += c;
106
+ }
107
+
108
+ continue;
109
+ }
110
+
111
+ if (c === ';' && inSelector) {
112
+ inSelector = true;
113
+ currentSelector = "";
114
+ continue;
115
+ }
116
+
117
+
118
+ if (inSelector === true && c !== '(') {
119
+ currentSelector += c;
120
+ continue;
121
+ }
122
+
123
+ if (c === '(' && inSelector) {
124
+ inSelector = false;
125
+ inKey = true;
126
+
127
+ currentKey = "";
128
+ continue;
129
+ }
130
+
131
+ if (inKey === true && c !== '=') {
132
+ currentKey += c;
133
+ continue;
134
+ }
135
+
136
+ if (c === '=' && inKey) {
137
+
138
+ inKey = false;
139
+ inValue = true;
140
+
141
+ if (nextChar === '"' || nextChar === "'") {
142
+ inQuotedValue = true;
143
+ quotedValueStartChar = nextChar;
144
+ i++;
145
+ continue;
146
+ }
147
+
148
+ currentValue = "";
149
+ continue;
150
+ }
151
+
152
+ if (inValue === true) {
153
+ if (inQuotedValue) {
154
+ if (c === quotedValueStartChar) {
155
+ inQuotedValue = false;
156
+ continue;
157
+ }
158
+
159
+ currentValue += c;
160
+ continue;
161
+ }
162
+
163
+ if (c === ',') {
164
+ inValue = false;
165
+ inKey = true;
166
+ const decodedCurrentValue = decodeURIComponent(currentValue);
167
+ addToResult(currentKey, decodedCurrentValue);
168
+ currentKey = "";
169
+ currentValue = "";
170
+ continue;
171
+ }
172
+
173
+ if (c === ')') {
174
+ inValue = false;
175
+ //inKey = true;
176
+ inSelector = true;
177
+
178
+ const decodedCurrentValue = decodeURIComponent(currentValue);
179
+ addToResult(currentKey, decodedCurrentValue);
180
+ currentKey = "";
181
+ currentValue = "";
182
+ currentSelector = "";
183
+ continue;
184
+ }
185
+
186
+ currentValue += c;
187
+
188
+ continue;
189
+ }
190
+ }
191
+
192
+
193
+ if (inSelector) {
194
+ return selectors;
195
+ }
196
+
197
+
198
+ return {};
199
+
200
+ }
201
+
202
+ /**
203
+ * Creates a hash selector string from an object.
204
+ *
205
+ * @param {Object} object - The object containing selectors and key-value pairs.
206
+ * @param {boolean} addHashPrefix - Whether to add the hash prefix # to the beginning of the string.
207
+ * @returns {string} The hash selector string.
208
+ * @since 3.37.0
209
+ */
210
+ function createBracketedKeyValueHash(object, addHashPrefix = true) {
211
+
212
+ if (!object) {
213
+ return addHashPrefix ? '#' : '';
214
+ }
215
+
216
+ let hashString = '';
217
+
218
+ function encodeKeyValue(key, value) {
219
+ return encodeURIComponent(key) + '=' + encodeURIComponent(value);
220
+ }
221
+
222
+ for (const selector in object) {
223
+ if (object.hasOwnProperty(selector)) {
224
+ const keyValuePairs = object[selector];
225
+ let selectorString = selector;
226
+ let keyValueString = '';
227
+
228
+ for (const key in keyValuePairs) {
229
+ if (keyValuePairs.hasOwnProperty(key)) {
230
+ const value = keyValuePairs[key];
231
+ keyValueString += keyValueString.length === 0 ? '' : ',';
232
+ keyValueString += encodeKeyValue(key, value);
233
+ }
234
+ }
235
+
236
+ if (keyValueString.length > 0) {
237
+ selectorString += '(' + keyValueString + ')';
238
+ hashString += hashString.length === 0 ? '' : ';';
239
+ hashString += selectorString;
240
+ }
241
+ }
242
+ }
243
+
244
+ return addHashPrefix ? '#' + hashString : hashString;
245
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Copyright schukai GmbH and contributors 2023. All Rights Reserved.
3
+ * Node module: @schukai/monster
4
+ * This file is licensed under the AGPLv3 License.
5
+ * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
6
+ */
7
+
8
+ export { generateRangeComparisonExpression };
9
+
10
+ /**
11
+ * The `generateRangeComparisonExpression()` function is function that generates a string representation
12
+ * of a comparison expression based on a range of values. It takes three arguments:
13
+ *
14
+ * - expression (required): a string representation of a range of values in the format of start1-end1,start2-end2,value3....
15
+ * - valueName (required): a string representing the name of the value that is being compared to the range of values.
16
+ * - options (optional): an object containing additional options to customize the comparison expression.
17
+ *
18
+ * The generateRangeComparisonExpression() function returns a string representation of the comparison expression.
19
+ *
20
+ * ## Options
21
+ * The options parameter is an object that can have the following properties:
22
+ *
23
+ * urlEncode (boolean, default: false): if set to true, URL encodes the comparison operators.
24
+ * andOp (string, default: '&&'): the logical AND operator to use in the expression.
25
+ * orOp (string, default: '||'): the logical OR operator to use in the expression.
26
+ * eqOp (string, default: '=='): the equality operator to use in the expression.
27
+ * geOp (string, default: '>='): the greater than or equal to operator to use in the expression.
28
+ * leOp (string, default: '<='): the less than or equal to operator to use in the expression.
29
+ *
30
+ * Examples
31
+ *
32
+ * ```javascript
33
+ * const expression = '0-10,20-30';
34
+ * const valueName = 'age';
35
+ * const options = { urlEncode: true, andOp: 'and', orOp: 'or', eqOp: '=', geOp: '>=', leOp: '<=' };
36
+ * const comparisonExpression = generateRangeComparisonExpression(expression, valueName, options);
37
+ *
38
+ * console.log(comparisonExpression); // age%3E%3D0%20and%20age%3C%3D10%20or%20age%3E%3D20%20and%20age%3C%3D30
39
+ * ```
40
+ *
41
+ * In this example, the generateRangeComparisonExpression() function generates a string representation of the comparison
42
+ * expression for the expression and valueName parameters with the specified options. The resulting comparison
43
+ * expression is 'age>=0 and age<=10 or age>=20 and age<=30', URL encoded according to the urlEncode option.
44
+ *
45
+ * @param {string} expression - The string expression to generate the comparison for.
46
+ * @param {string} valueName - The name of the value to compare against.
47
+ * @param {Object} [options] - The optional parameters.
48
+ * @param {boolean} [options.urlEncode=false] - Whether to encode comparison operators for use in a URL.
49
+ * @param {string} [options.andOp='&&'] - The logical AND operator to use.
50
+ * @param {string} [options.orOp='||'] - The logical OR operator to use.
51
+ * @param {string} [options.eqOp='=='] - The comparison operator for equality to use.
52
+ * @param {string} [options.geOp='>='] - The comparison operator for greater than or equal to to use.
53
+ * @param {string} [options.leOp='<='] - The comparison operator for less than or equal to to use.
54
+ * @returns {string} The generated comparison expression.
55
+ * @throws {Error} If the input is invalid.
56
+ * @memberOf Monster.Text
57
+ * @summary Generates a comparison expression based on a range of values.
58
+ */
59
+ function generateRangeComparisonExpression(expression, valueName, options = {}) {
60
+ const { urlEncode = false, andOp = "&&", orOp = "||", eqOp = "==", geOp = ">=", leOp = "<=" } = options;
61
+ const ranges = expression.split(",");
62
+ let comparison = "";
63
+ for (let i = 0; i < ranges.length; i++) {
64
+ const range = ranges[i].trim();
65
+ if (range === "") {
66
+ throw new Error(`Invalid range '${range}'`);
67
+ } else if (range.includes("-")) {
68
+ const [start, end] = range.split("-").map((s) => (s === "" ? null : parseFloat(s)));
69
+ if ((start !== null && isNaN(start)) || (end !== null && isNaN(end))) {
70
+ throw new Error(`Invalid value in range '${range}'`);
71
+ }
72
+ if (start !== null && end !== null && start > end) {
73
+ throw new Error(`Invalid range '${range}'`);
74
+ }
75
+ const compStart =
76
+ start !== null ? `${valueName}${urlEncode ? encodeURIComponent(geOp) : geOp}${start}` : "";
77
+ const compEnd = end !== null ? `${valueName}${urlEncode ? encodeURIComponent(leOp) : leOp}${end}` : "";
78
+ const compRange = `${compStart}${compStart && compEnd ? ` ${andOp} ` : ""}${compEnd}`;
79
+ comparison += ranges.length > 1 ? `(${compRange})` : compRange;
80
+ } else {
81
+ const value = parseFloat(range);
82
+ if (isNaN(value)) {
83
+ throw new Error(`Invalid value '${range}'`);
84
+ }
85
+ const compValue = `${valueName}${urlEncode ? encodeURIComponent(eqOp) : eqOp}${value}`;
86
+ comparison += ranges.length > 1 ? `(${compValue})` : compValue;
87
+ }
88
+ if (i < ranges.length - 1) {
89
+ comparison += ` ${orOp} `;
90
+ }
91
+ }
92
+ return comparison;
93
+ }
@@ -1,86 +1,2 @@
1
- export { generateRangeComparisonExpression };
1
+ export { generateRangeComparisonExpression } from "./generate-range-comparison-expression.mjs"
2
2
 
3
- /**
4
- * The `generateRangeComparisonExpression()` function is function that generates a string representation
5
- * of a comparison expression based on a range of values. It takes three arguments:
6
- *
7
- * - expression (required): a string representation of a range of values in the format of start1-end1,start2-end2,value3....
8
- * - valueName (required): a string representing the name of the value that is being compared to the range of values.
9
- * - options (optional): an object containing additional options to customize the comparison expression.
10
- *
11
- * The generateRangeComparisonExpression() function returns a string representation of the comparison expression.
12
- *
13
- * ## Options
14
- * The options parameter is an object that can have the following properties:
15
- *
16
- * urlEncode (boolean, default: false): if set to true, URL encodes the comparison operators.
17
- * andOp (string, default: '&&'): the logical AND operator to use in the expression.
18
- * orOp (string, default: '||'): the logical OR operator to use in the expression.
19
- * eqOp (string, default: '=='): the equality operator to use in the expression.
20
- * geOp (string, default: '>='): the greater than or equal to operator to use in the expression.
21
- * leOp (string, default: '<='): the less than or equal to operator to use in the expression.
22
- *
23
- * Examples
24
- *
25
- * ```javascript
26
- * const expression = '0-10,20-30';
27
- * const valueName = 'age';
28
- * const options = { urlEncode: true, andOp: 'and', orOp: 'or', eqOp: '=', geOp: '>=', leOp: '<=' };
29
- * const comparisonExpression = generateRangeComparisonExpression(expression, valueName, options);
30
- *
31
- * console.log(comparisonExpression); // age%3E%3D0%20and%20age%3C%3D10%20or%20age%3E%3D20%20and%20age%3C%3D30
32
- * ```
33
- *
34
- * In this example, the generateRangeComparisonExpression() function generates a string representation of the comparison
35
- * expression for the expression and valueName parameters with the specified options. The resulting comparison
36
- * expression is 'age>=0 and age<=10 or age>=20 and age<=30', URL encoded according to the urlEncode option.
37
- *
38
- * @param {string} expression - The string expression to generate the comparison for.
39
- * @param {string} valueName - The name of the value to compare against.
40
- * @param {Object} [options] - The optional parameters.
41
- * @param {boolean} [options.urlEncode=false] - Whether to encode comparison operators for use in a URL.
42
- * @param {string} [options.andOp='&&'] - The logical AND operator to use.
43
- * @param {string} [options.orOp='||'] - The logical OR operator to use.
44
- * @param {string} [options.eqOp='=='] - The comparison operator for equality to use.
45
- * @param {string} [options.geOp='>='] - The comparison operator for greater than or equal to to use.
46
- * @param {string} [options.leOp='<='] - The comparison operator for less than or equal to to use.
47
- * @returns {string} The generated comparison expression.
48
- * @throws {Error} If the input is invalid.
49
- * @memberOf Monster.Text
50
- * @summary Generates a comparison expression based on a range of values.
51
- */
52
- function generateRangeComparisonExpression(expression, valueName, options = {}) {
53
- const { urlEncode = false, andOp = "&&", orOp = "||", eqOp = "==", geOp = ">=", leOp = "<=" } = options;
54
- const ranges = expression.split(",");
55
- let comparison = "";
56
- for (let i = 0; i < ranges.length; i++) {
57
- const range = ranges[i].trim();
58
- if (range === "") {
59
- throw new Error(`Invalid range '${range}'`);
60
- } else if (range.includes("-")) {
61
- const [start, end] = range.split("-").map((s) => (s === "" ? null : parseFloat(s)));
62
- if ((start !== null && isNaN(start)) || (end !== null && isNaN(end))) {
63
- throw new Error(`Invalid value in range '${range}'`);
64
- }
65
- if (start !== null && end !== null && start > end) {
66
- throw new Error(`Invalid range '${range}'`);
67
- }
68
- const compStart =
69
- start !== null ? `${valueName}${urlEncode ? encodeURIComponent(geOp) : geOp}${start}` : "";
70
- const compEnd = end !== null ? `${valueName}${urlEncode ? encodeURIComponent(leOp) : leOp}${end}` : "";
71
- const compRange = `${compStart}${compStart && compEnd ? ` ${andOp} ` : ""}${compEnd}`;
72
- comparison += ranges.length > 1 ? `(${compRange})` : compRange;
73
- } else {
74
- const value = parseFloat(range);
75
- if (isNaN(value)) {
76
- throw new Error(`Invalid value '${range}'`);
77
- }
78
- const compValue = `${valueName}${urlEncode ? encodeURIComponent(eqOp) : eqOp}${value}`;
79
- comparison += ranges.length > 1 ? `(${compValue})` : compValue;
80
- }
81
- if (i < ranges.length - 1) {
82
- comparison += ` ${orOp} `;
83
- }
84
- }
85
- return comparison;
86
- }
@@ -142,7 +142,7 @@ function getMonsterVersion() {
142
142
  }
143
143
 
144
144
  /** don't touch, replaced by make with package.json version */
145
- monsterVersion = new Version("3.36.0");
145
+ monsterVersion = new Version("3.37.0");
146
146
 
147
147
  return monsterVersion;
148
148
  }
@@ -7,7 +7,7 @@ describe('Monster', function () {
7
7
  let monsterVersion
8
8
 
9
9
  /** don´t touch, replaced by make with package.json version */
10
- monsterVersion = new Version("3.36.0")
10
+ monsterVersion = new Version("3.37.0")
11
11
 
12
12
  let m = getMonsterVersion();
13
13
 
@@ -0,0 +1,214 @@
1
+ // test.js
2
+ import {expect} from "chai";
3
+ import {
4
+ parseBracketedKeyValueHash,
5
+ createBracketedKeyValueHash
6
+ } from "../../../../application/source/text/bracketed-key-value-hash.mjs";
7
+
8
+ describe("parseBracketedKeyValueHash", () => {
9
+ it("should return an empty object for an empty string", () => {
10
+ const input = "";
11
+ const expectedResult = {};
12
+ expect(parseBracketedKeyValueHash(input)).to.deep.equal(expectedResult);
13
+ });
14
+
15
+ it("should parse a single selector with one key-value pair", () => {
16
+ const input = "#selector1(key1=value1)";
17
+ const expectedResult = {
18
+ selector1: {
19
+ key1: "value1",
20
+ },
21
+ };
22
+ expect(parseBracketedKeyValueHash(input)).to.deep.equal(expectedResult);
23
+ });
24
+
25
+ it("should parse multiple selectors with multiple key-value pairs", () => {
26
+ const input = "#selector1(key1=value1,key2=value2);selector2(key3=value3,key4=value4)";
27
+ const expectedResult = {
28
+ selector1: {
29
+ key1: "value1",
30
+ key2: "value2",
31
+ },
32
+ selector2: {
33
+ key3: "value3",
34
+ key4: "value4",
35
+ },
36
+ };
37
+ expect(parseBracketedKeyValueHash(input)).to.deep.equal(expectedResult);
38
+ });
39
+
40
+ it("should decode URL-encoded values", () => {
41
+ const input = "#selector1(key1=value1%2Cwith%20comma)";
42
+ const expectedResult = {
43
+ selector1: {
44
+ key1: "value1,with comma",
45
+ },
46
+ };
47
+ const result = parseBracketedKeyValueHash(input);
48
+ expect(result.selector1.key1).to.equal(expectedResult.selector1.key1);
49
+ });
50
+
51
+ it("should handle input without a leading hash", () => {
52
+ const input = "selector1(key1=value1)";
53
+ const expectedResult = {
54
+ selector1: {
55
+ key1: "value1",
56
+ },
57
+ };
58
+ expect(parseBracketedKeyValueHash(input)).to.deep.equal(expectedResult);
59
+ });
60
+
61
+ it("should return an empty object for invalid input", () => {
62
+ const input = "#selector1(key1=value1,key2";
63
+ const expectedResult = {};
64
+ expect(parseBracketedKeyValueHash(input)).to.deep.equal(expectedResult);
65
+ });
66
+
67
+ it('should return an empty object for an empty input string', () => {
68
+ const hashString = '';
69
+ const result = parseBracketedKeyValueHash(hashString);
70
+ expect(result).to.deep.equal({});
71
+ });
72
+
73
+ it('should return an empty object for an invalid input string', () => {
74
+ const hashString = '#invalid';
75
+ const result = parseBracketedKeyValueHash(hashString);
76
+ expect(result).to.deep.equal({});
77
+ });
78
+
79
+ it('should parse a simple input string with one selector and one key-value pair', () => {
80
+ const hashString = '#selector(key=value)';
81
+ const result = parseBracketedKeyValueHash(hashString);
82
+ expect(result).to.deep.equal({selector: {key: 'value'}});
83
+ });
84
+
85
+ it('should parse an input string with multiple selectors and key-value pairs', () => {
86
+ const hashString = '#selector1(key1=value1);selector2(key2=value2)';
87
+ const result = parseBracketedKeyValueHash(hashString);
88
+ expect(result).to.deep.equal({selector1: {key1: 'value1'}, selector2: {key2: 'value2'}});
89
+ });
90
+
91
+ it('should handle empty values', () => {
92
+ const hashString = '#selector(key1=,key2=)';
93
+ const result = parseBracketedKeyValueHash(hashString);
94
+ expect(result).to.deep.equal({selector: {key1: '', key2: ''}});
95
+ });
96
+
97
+ it('should handle percent-encoded values', () => {
98
+ const hashString = '#selector(key1=value%201,key2=value%2C2)';
99
+ const result = parseBracketedKeyValueHash(hashString);
100
+ expect(result).to.deep.equal({selector: {key1: 'value 1', key2: 'value,2'}});
101
+ });
102
+
103
+ it('should handle double-quoted values with commas', () => {
104
+ const hashString = '#selector(key1="value,1",key2="value,2")';
105
+ const result = parseBracketedKeyValueHash(hashString);
106
+ expect(result).to.deep.equal({selector: {key1: 'value,1', key2: 'value,2'}});
107
+ });
108
+
109
+ it('should ignore leading hash symbol (#)', () => {
110
+ const hashString = 'selector(key=value)';
111
+ const result = parseBracketedKeyValueHash(hashString);
112
+ expect(result).to.deep.equal({selector: {key: 'value'}});
113
+ });
114
+
115
+ it('should ignore leading and trailing white space', () => {
116
+ const hashString = ' #selector(key=value) ';
117
+ const result = parseBracketedKeyValueHash(hashString);
118
+ expect(result).to.deep.equal({selector: {key: 'value'}});
119
+ });
120
+
121
+ it('should return an empty object if the input string ends prematurely', () => {
122
+ const hashString = '#selector(key=value';
123
+ const result = parseBracketedKeyValueHash(hashString);
124
+ expect(result).to.deep.equal({});
125
+ });
126
+
127
+ it('should return an empty object if a selector is missing', () => {
128
+ const hashString = '#(key=value)';
129
+ const result = parseBracketedKeyValueHash(hashString);
130
+ expect(result).to.deep.equal({});
131
+ });
132
+
133
+ it('should return an empty object if a key is missing', () => {
134
+ const hashString = '#selector(=value)';
135
+ const result = parseBracketedKeyValueHash(hashString);
136
+ expect(result).to.deep.equal({});
137
+ });
138
+
139
+ it('should return an empty object ifa value is missing', () => {
140
+ const hashString = '#selector(key=)';
141
+ const result = parseBracketedKeyValueHash(hashString);
142
+ expect(result).to.deep.equal({
143
+ selector: {
144
+ key: '',
145
+ },
146
+ });
147
+ });
148
+
149
+ it('should return an empty object if there is no closing parenthesis for a selector', () => {
150
+ const hashString = '#selector(key=value;';
151
+ const result = parseBracketedKeyValueHash(hashString);
152
+ expect(result).to.deep.equal({});
153
+ });
154
+
155
+ it('should return an empty object if there is no semicolon after a selector', () => {
156
+ const hashString = '#selector(key=value)selector2(key2=value2)';
157
+ const result = parseBracketedKeyValueHash(hashString);
158
+ expect(result).to.deep.equal({
159
+ selector: {
160
+ key: 'value',
161
+ },
162
+ selector2: {
163
+ key2: 'value2',
164
+ },
165
+ });
166
+ });
167
+
168
+ describe('createBracketedKeyValueHash', () => {
169
+ it('should return an hash string for a simple object', () => {
170
+ const input = {
171
+ '.example': {
172
+ 'color': 'red',
173
+ 'font-size': '14px'
174
+ },
175
+ '.other': {
176
+ 'background': 'blue'
177
+ }
178
+ };
179
+
180
+ const result = createBracketedKeyValueHash(input);
181
+ expect(result).to.deep.equal("#.example(color=red,font-size=14px);.other(background=blue)");
182
+ });
183
+
184
+ it('should return a url-encoded hash string for a simple object', () => {
185
+ const input = {
186
+ '.example': {
187
+ 'color': 'r"ed',
188
+ 'font-size': '14px'
189
+ },
190
+ '.other': {
191
+ 'background': 'blue'
192
+ }
193
+ };
194
+
195
+ const result = createBracketedKeyValueHash(input, true);
196
+ expect(result).to.deep.equal("#.example(color=r%22ed,font-size=14px);.other(background=blue)");
197
+ });
198
+
199
+ it('should return an empty string for an empty object', () => {
200
+ const input = {};
201
+ const result = createBracketedKeyValueHash(input,false);
202
+ expect(result).to.deep.equal("");
203
+ });
204
+
205
+ it('should return an empty string for an empty object', () => {
206
+ const input = {};
207
+ const result = createBracketedKeyValueHash(input,false);
208
+ expect(result).to.deep.equal("");
209
+ });
210
+
211
+ });
212
+
213
+
214
+ });