@schukai/monster 3.35.4 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schukai/monster",
3
- "version": "3.35.4",
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",
@@ -8,7 +8,7 @@
8
8
  import { getGlobal } from "../types/global.mjs";
9
9
  import { validateString } from "../types/validate.mjs";
10
10
 
11
- export { getDocument, getWindow, getDocumentFragmentFromString, findElementWithIdUpwards };
11
+ export { getDocument, getWindow, getDocumentFragmentFromString, findElementWithIdUpwards,getContainingDocument };
12
12
 
13
13
  /**
14
14
  * This method fetches the document object
@@ -199,3 +199,48 @@ function findElementWithIdUpwards(element, targetId) {
199
199
  // Otherwise, search the current element's parent
200
200
  return findElementWithIdUpwards(element.parentElement, targetId);
201
201
  }
202
+
203
+ /**
204
+ * @private
205
+ * @param {HTMLElement} element
206
+ * @returns {HTMLElement|null}
207
+ */
208
+ function traverseShadowRoots(element) {
209
+ let currentRoot = element.shadowRoot;
210
+ let currentParent = element.parentNode;
211
+
212
+ while (currentParent && currentParent.nodeType !== Node.DOCUMENT_NODE && currentParent.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
213
+ if (currentRoot && currentRoot.parentNode) {
214
+ currentParent = currentRoot.parentNode;
215
+ currentRoot = currentParent.shadowRoot;
216
+ } else if (currentParent.parentNode) {
217
+ currentParent = currentParent.parentNode;
218
+ currentRoot = null;
219
+ } else if (currentRoot && currentRoot.host && currentRoot.host.nodeType === Node.DOCUMENT_NODE) {
220
+ currentParent = currentRoot.host;
221
+ currentRoot = null;
222
+ } else {
223
+ currentParent = null;
224
+ currentRoot = null;
225
+ }
226
+ }
227
+
228
+ return currentParent;
229
+ }
230
+
231
+ /**
232
+ * Recursively searches upwards from a given element to find an ancestor element
233
+ *
234
+ * @param {HTMLElement} element
235
+ * @returns {*}
236
+ * @throws {Error} Invalid argument. Expected an HTMLElement.
237
+ * @memberOf Monster.DOM
238
+ * @since 3.36.0
239
+ */
240
+ function getContainingDocument(element) {
241
+ if (!element || !(element instanceof HTMLElement || element instanceof element.ownerDocument.defaultView.HTMLElement)) {
242
+ throw new Error('Invalid argument. Expected an HTMLElement.');
243
+ }
244
+
245
+ return traverseShadowRoots(element) || null;
246
+ }
@@ -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.35.4");
145
+ monsterVersion = new Version("3.37.0");
146
146
 
147
147
  return monsterVersion;
148
148
  }
@@ -2,6 +2,7 @@ import {
2
2
  getDocument, getWindow, getDocumentFragmentFromString
3
3
  } from "../../../../application/source/dom/util.mjs";
4
4
 
5
+ import {getContainingDocument} from "../../../../application/source/dom/util.mjs";
5
6
 
6
7
  import {initJSDOM} from "../../util/jsdom.mjs";
7
8
 
@@ -52,4 +53,63 @@ describe('DOM', function () {
52
53
  });
53
54
 
54
55
  });
56
+
57
+
58
+ describe('getContainingDocument', () => {
59
+ let jsDomDocument;
60
+
61
+ beforeEach(() => {
62
+ jsDomDocument = getDocument();
63
+ });
64
+ //
65
+ // afterEach(() => {
66
+ // dom.window.close();
67
+ // });
68
+
69
+ it('should throw an error when called with an invalid argument', () => {
70
+ expect(() => getContainingDocument(null)).to.throw('Invalid argument. Expected an HTMLElement.');
71
+ });
72
+
73
+ it('should return the correct containing document for an element in the main document', () => {
74
+ const element = jsDomDocument.createElement('div');
75
+ const containingDocument = getContainingDocument(element);
76
+
77
+ expect(containingDocument).to.null;
78
+ });
79
+
80
+ it('should return the correct containing document for an element inside a shadow root', () => {
81
+ const host = jsDomDocument.createElement('div');
82
+ const shadowRoot = host.attachShadow({ mode: 'open' });
83
+ const element = jsDomDocument.createElement('span');
84
+ shadowRoot.appendChild(element);
85
+
86
+ const containingDocument = getContainingDocument(element);
87
+ expect(containingDocument).to.not.null;
88
+ });
89
+
90
+ it('should return the correct containing document for an element inside a nested shadow root', () => {
91
+ const outerHost = jsDomDocument.createElement('div');
92
+ const outerShadowRoot = outerHost.attachShadow({ mode: 'open' });
93
+
94
+ const innerHost = jsDomDocument.createElement('div');
95
+ outerShadowRoot.appendChild(innerHost);
96
+
97
+ const innerShadowRoot = innerHost.attachShadow({ mode: 'open' });
98
+
99
+ const element = jsDomDocument.createElement('span');
100
+ innerShadowRoot.appendChild(element);
101
+
102
+ const containingDocument = getContainingDocument(element);
103
+ expect(containingDocument).to.not.null;
104
+ });
105
+
106
+ it('should return null when the element is not attached to any document', () => {
107
+ const detachedElement = jsDomDocument.createElement('div');
108
+ detachedElement.remove();
109
+
110
+ const containingDocument = getContainingDocument(detachedElement);
111
+ expect(containingDocument).to.be.null;
112
+ });
113
+ });
114
+
55
115
  });
@@ -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.35.4")
10
+ monsterVersion = new Version("3.37.0")
11
11
 
12
12
  let m = getMonsterVersion();
13
13