@schukai/monster 3.36.0 → 3.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/source/text/bracketed-key-value-hash.mjs +245 -0
- package/source/text/generate-range-comparison-expression.mjs +93 -0
- package/source/text/util.mjs +1 -85
- package/source/types/version.mjs +1 -1
- package/test/cases/monster.mjs +1 -1
- package/test/cases/text/bracketed-key-value-hash.mjs +214 -0
package/package.json
CHANGED
@@ -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
|
+
}
|
package/source/text/util.mjs
CHANGED
@@ -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
|
-
}
|
package/source/types/version.mjs
CHANGED
package/test/cases/monster.mjs
CHANGED
@@ -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
|
+
});
|