@schukai/monster 3.35.4 → 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.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