@schukai/monster 3.36.0 → 3.38.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.38.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,78 @@
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
+ import {Pathfinder} from '../../data/pathfinder.mjs';
9
+ import {isFunction} from '../../types/is.mjs';
10
+
11
+ export {initOptionsFromAttributes };
12
+
13
+ /**
14
+ * Initializes the given options object based on the attributes of the current DOM element.
15
+ * The function looks for attributes with the prefix 'data-monster-option-', and maps them to
16
+ * properties in the options object. It replaces the dashes with dots to form the property path.
17
+ * For example, the attribute 'data-monster-option-url' maps to the 'url' property in the options object.
18
+ *
19
+ * With the mapping parameter, the attribute value can be mapped to a different value.
20
+ * For example, the attribute 'data-monster-option-foo' maps to the 'bar' property in the options object.
21
+ *
22
+ * The mapping object would look like this:
23
+ * {
24
+ * 'foo': (value) => value + 'bar'
25
+ * // the value of the attribute 'data-monster-option-foo' is appended with 'bar'
26
+ * // and assigned to the 'bar' property in the options object.
27
+ * // e.g. <div data-monster-option-foo="foo"></div>
28
+ * 'bar.baz': (value) => value + 'bar'
29
+ * // the value of the attribute 'data-monster-option-bar-baz' is appended with 'bar'
30
+ * // and assigned to the 'bar.baz' property in the options object.
31
+ * // e.g. <div data-monster-option-bar-baz="foo"></div>
32
+ * }
33
+ *
34
+ * @param {HTMLElement} element - The DOM element to be used as the source of the attributes.
35
+ * @param {Object} options - The options object to be initialized.
36
+ * @param {Object} mapping - A mapping between the attribute value and the property value.
37
+ * @param {string} prefix - The prefix of the attributes to be considered.
38
+ * @returns {Object} - The initialized options object.
39
+ * @this HTMLElement - The context of the DOM element.
40
+ */
41
+ function initOptionsFromAttributes(element, options, mapping={},prefix = 'data-monster-option-') {
42
+ if (!(element instanceof HTMLElement)) return options;
43
+ if (!element.hasAttributes()) return options;
44
+
45
+ const finder = new Pathfinder(options);
46
+
47
+ element.getAttributeNames().forEach((name) => {
48
+ if (!name.startsWith(prefix)) return;
49
+
50
+ // check if the attribute name is a valid option.
51
+ // the mapping between the attribute is simple. The dash is replaced by a dot.
52
+ // e.g. data-monster-url => url
53
+ const optionName = name.replace(prefix, '').replace(/-/g, '.');
54
+ if (!finder.exists(optionName)) return;
55
+
56
+ if (element.hasAttribute(name)) {
57
+ let value = element.getAttribute(name);
58
+ if (mapping.hasOwnProperty(optionName)&&isFunction(mapping[optionName])) {
59
+ value = mapping[optionName](value);
60
+ }
61
+
62
+ const typeOfOptionValue = typeof finder.getVia(optionName);
63
+ if (typeOfOptionValue === 'boolean') {
64
+ value = value === 'true';
65
+ } else if (typeOfOptionValue === 'number') {
66
+ value = Number(value);
67
+ } else if (typeOfOptionValue === 'string') {
68
+ value = String(value);
69
+ } else if (typeOfOptionValue === 'object') {
70
+ value = JSON.parse(value);
71
+ }
72
+
73
+ finder.setVia(optionName, value);
74
+ }
75
+ })
76
+
77
+ return options;
78
+ }
@@ -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.38.0");
146
146
 
147
147
  return monsterVersion;
148
148
  }
@@ -8,7 +8,7 @@ import {initJSDOM} from "../../util/jsdom.mjs";
8
8
  describe('Events', function () {
9
9
 
10
10
  before(async function () {
11
- initJSDOM();
11
+ await initJSDOM();
12
12
  })
13
13
 
14
14
  describe('findTargetElementFromEvent()', function () {
@@ -23,10 +23,10 @@ describe('Events', function () {
23
23
  expect(e.getAttribute('data-monster')).to.be.equal('hello')
24
24
  done();
25
25
  })
26
- setTimeout(()=>{
26
+ setTimeout(() => {
27
27
  fireEvent(div, 'click');
28
- },0)
29
-
28
+ }, 0)
29
+
30
30
  });
31
31
 
32
32
  });
@@ -76,10 +76,10 @@ describe('Events', function () {
76
76
 
77
77
  it('should throw error', function () {
78
78
  expect(() => fireEvent({}, 'touch')).to.throw(Error);
79
-
79
+
80
80
  });
81
- });
82
-
81
+ });
82
+
83
83
  describe('fireCustomEvent()', function () {
84
84
  it('should fire a click event', function (done) {
85
85
  let div = document.createElement('div');
@@ -100,8 +100,8 @@ describe('Events', function () {
100
100
  it('should fire a touch event on collection1', function (done) {
101
101
  let div = document.createElement('div');
102
102
  div.addEventListener('touch', (e) => {
103
- if(e.detail.detail!=='hello world') {
104
- done('error');
103
+ if (e.detail.detail !== 'hello world') {
104
+ done('error');
105
105
  }
106
106
  done();
107
107
  })
@@ -111,12 +111,12 @@ describe('Events', function () {
111
111
 
112
112
  fireCustomEvent(collection, 'touch', "hello world");
113
113
  });
114
-
114
+
115
115
  it('should fire a touch event on collection2', function (done) {
116
116
  let div = document.createElement('div');
117
117
  div.addEventListener('touch', (e) => {
118
- if(e.detail.a!=='hello world') {
119
- done('error');
118
+ if (e.detail.a !== 'hello world') {
119
+ done('error');
120
120
  }
121
121
  done();
122
122
  })
@@ -124,7 +124,7 @@ describe('Events', function () {
124
124
  div.appendChild(document.createElement('div'));
125
125
  let collection = div.querySelectorAll('div');
126
126
 
127
- fireCustomEvent(collection, 'touch', {a:"hello world"});
127
+ fireCustomEvent(collection, 'touch', {a: "hello world"});
128
128
  });
129
129
 
130
130
  it('should fire a touch event', function (done) {
@@ -0,0 +1,155 @@
1
+ import {expect} from "chai"
2
+
3
+ import {initOptionsFromAttributes} from "../../../../..//application/source/dom/util/init-options-from-attributes.mjs";
4
+ import {initJSDOM} from "../../../util/jsdom.mjs";
5
+
6
+ describe('initOptionsFromAttributes', () => {
7
+ let element;
8
+ let options;
9
+
10
+ before(async function () {
11
+ await initJSDOM();
12
+ })
13
+
14
+ beforeEach(() => {
15
+ options = { url: "", key: { subkey: "" } };
16
+ element = document.createElement('div');
17
+ });
18
+
19
+ it('should initialize options with matching attributes', () => {
20
+ element.setAttribute('data-monster-option-url', 'https://example.com');
21
+ element.setAttribute('data-monster-option-key.subkey', 'test');
22
+
23
+ const result = initOptionsFromAttributes(element, options);
24
+
25
+ expect(result.url).to.equal('https://example.com');
26
+ expect(result.key.subkey).to.equal('test');
27
+ });
28
+
29
+ it('should not modify options without matching attributes', () => {
30
+ const result = initOptionsFromAttributes(element, options);
31
+
32
+ expect(result.url).to.equal('');
33
+ expect(result.key.subkey).to.equal('');
34
+ });
35
+
36
+ it('should ignore attributes without the correct prefix', () => {
37
+ element.setAttribute('data-some-option-url', 'https://example.com');
38
+
39
+ const result = initOptionsFromAttributes(element, options);
40
+
41
+ expect(result.url).to.equal('');
42
+ });
43
+
44
+ it('should ignore attributes with invalid option paths', () => {
45
+ element.setAttribute('data-monster-option-nonexistent', 'value');
46
+
47
+ const result = initOptionsFromAttributes(element, options);
48
+
49
+ expect(result).to.deep.equal(options);
50
+ });
51
+
52
+ it('should apply mapping for a single attribute', () => {
53
+ element.setAttribute('data-monster-option-url', 'example');
54
+ const mapping = {
55
+ 'url': (value) => 'https://' + value + '.com'
56
+ };
57
+
58
+ const result = initOptionsFromAttributes(element, options, mapping);
59
+
60
+ expect(result.url).to.equal('https://example.com');
61
+ });
62
+
63
+ it('should apply mapping for a nested attribute', () => {
64
+ element.setAttribute('data-monster-option-key-subkey', '123');
65
+ const mapping = {
66
+ 'key.subkey': (value) => parseInt(value, 10) * 2
67
+ };
68
+
69
+ const result = initOptionsFromAttributes(element, options, mapping);
70
+
71
+ expect(result.key.subkey).to.equal("246");
72
+ });
73
+
74
+ it('should apply multiple mappings', () => {
75
+ element.setAttribute('data-monster-option-url', 'example');
76
+ element.setAttribute('data-monster-option-key.subkey', '123');
77
+ const mapping = {
78
+ 'url': (value) => 'https://' + value + '.com',
79
+ 'key.subkey': (value) => parseInt(value, 10) * 2
80
+ };
81
+
82
+ const result = initOptionsFromAttributes(element, options, mapping);
83
+
84
+ expect(result.url).to.equal('https://example.com');
85
+ expect(result.key.subkey).to.equal("246");
86
+ });
87
+
88
+ it('should ignore mappings for non-existing attributes', () => {
89
+ const mapping = {
90
+ 'url': (value) => 'https://' + value + '.com'
91
+ };
92
+
93
+ const result = initOptionsFromAttributes(element, options, mapping);
94
+
95
+ expect(result.url).to.equal('');
96
+ });
97
+
98
+ it('should ignore mappings for invalid option paths', () => {
99
+ element.setAttribute('data-monster-option-nonexistent', 'value');
100
+ const mapping = {
101
+ 'nonexistent': (value) => value + 'bar'
102
+ };
103
+
104
+ const result = initOptionsFromAttributes(element, options, mapping);
105
+
106
+ expect(result).to.deep.equal(options);
107
+ });
108
+
109
+ it('should apply mapping only to specified attributes', () => {
110
+ element.setAttribute('data-monster-option-url', 'example');
111
+ element.setAttribute('data-monster-option-key.subkey', '123');
112
+ const mapping = {
113
+ 'url': (value) => 'https://' + value + '.com'
114
+ };
115
+
116
+ const result = initOptionsFromAttributes(element, options, mapping);
117
+
118
+ expect(result.url).to.equal('https://example.com');
119
+ expect(result.key.subkey).to.equal('123');
120
+ });
121
+
122
+ it('should not apply mapping if not a function', () => {
123
+ element.setAttribute('data-monster-option-url', 'example');
124
+ const mapping = {
125
+ 'url': 'https://example.com'
126
+ };
127
+
128
+ const result = initOptionsFromAttributes(element, options, mapping);
129
+
130
+ expect(result.url).to.equal('example');
131
+ });
132
+
133
+ it('should apply mapping with custom prefix', () => {
134
+ element.setAttribute('data-custom-option-url', 'example');
135
+ const mapping = {
136
+ 'url': (value) => 'https://' + value + '.com'
137
+ };
138
+
139
+ const result = initOptionsFromAttributes(element, options, mapping, 'data-custom-option-');
140
+
141
+ expect(result.url).to.equal('https://example.com');
142
+ });
143
+
144
+ it('should not apply mapping with incorrect custom prefix', () => {
145
+ element.setAttribute('data-custom-option-url', 'example');
146
+ const mapping = {
147
+ 'url': (value) => 'https://' + value + '.com'
148
+ };
149
+
150
+ const result = initOptionsFromAttributes(element, options, mapping);
151
+
152
+ expect(result.url).to.equal('');
153
+ });
154
+
155
+ });
@@ -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.38.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
+ });