@schukai/monster 3.29.0 → 3.31.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.29.0",
3
+ "version": "3.31.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 };
11
+ export { getDocument, getWindow, getDocumentFragmentFromString, findElementWithIdUpwards };
12
12
 
13
13
  /**
14
14
  * This method fetches the document object
@@ -151,3 +151,52 @@ function getDocumentFragmentFromString(html) {
151
151
 
152
152
  return template.content;
153
153
  }
154
+
155
+
156
+ /**
157
+ * Recursively searches upwards from a given element to find an ancestor element
158
+ * with a specified ID, considering both normal DOM and shadow DOM.
159
+ *
160
+ * @param {HTMLElement|ShadowRoot} element - The starting element or shadow root to search from.
161
+ * @param {string} targetId - The ID of the target element to find.
162
+ * @returns {HTMLElement|null} - The ancestor element with the specified ID, or null if not found.
163
+ * @memberOf Monster.DOM
164
+ * @since 3.29.0
165
+ * @license AGPLv3
166
+ * @copyright schukai GmbH
167
+ */
168
+ function findElementWithIdUpwards(element, targetId) {
169
+ if (!element) {
170
+ return null;
171
+ }
172
+
173
+ // Check if the current element has the target ID
174
+ if (element.id === targetId) {
175
+ return element;
176
+ }
177
+
178
+ // Search within the current element's shadow root, if it exists
179
+ if (element.shadowRoot) {
180
+ const target = element.shadowRoot.getElementById(targetId);
181
+ if (target) {
182
+ return target;
183
+ }
184
+ }
185
+
186
+ // If the current element is the document.documentElement, search within the main document
187
+ if (element === document.documentElement) {
188
+ const target = document.getElementById(targetId);
189
+ if (target) {
190
+ return target;
191
+ }
192
+ }
193
+
194
+ // If the current element is inside a shadow root, search its host's ancestors
195
+ const rootNode = element.getRootNode();
196
+ if (rootNode && rootNode instanceof ShadowRoot) {
197
+ return findElementWithIdUpwards(rootNode.host, targetId);
198
+ }
199
+
200
+ // Otherwise, search the current element's parent
201
+ return findElementWithIdUpwards(element.parentElement, targetId);
202
+ }
@@ -0,0 +1,57 @@
1
+ export {generateRangeComparisonExpression}
2
+
3
+ /**
4
+ * Generates a comparison expression for a comma-separated string of ranges and single values.
5
+ * @param {string} expression - The string expression to generate the comparison for.
6
+ * @param {string} valueName - The name of the value to compare against.
7
+ * @param {Object} [options] - The optional parameters.
8
+ * @param {boolean} [options.urlEncode=false] - Whether to encode comparison operators for use in a URL.
9
+ * @param {string} [options.andOp='&&'] - The logical AND operator to use.
10
+ * @param {string} [options.orOp='||'] - The logical OR operator to use.
11
+ * @param {string} [options.eqOp='=='] - The comparison operator for equality to use.
12
+ * @param {string} [options.geOp='>='] - The comparison operator for greater than or equal to to use.
13
+ * @param {string} [options.leOp='<='] - The comparison operator for less than or equal to to use.
14
+ * @returns {string} The generated comparison expression.
15
+ * @throws {Error} If the input is invalid.
16
+ */
17
+ function generateRangeComparisonExpression(expression, valueName, options = {}) {
18
+ const {
19
+ urlEncode = false,
20
+ andOp = '&&',
21
+ orOp = '||',
22
+ eqOp = '==',
23
+ geOp = '>=',
24
+ leOp = '<=',
25
+ } = options;
26
+ const ranges = expression.split(',');
27
+ let comparison = '';
28
+ for (let i = 0; i < ranges.length; i++) {
29
+ const range = ranges[i].trim();
30
+ if (range === '') {
31
+ throw new Error(`Invalid range '${range}'`);
32
+ } else if (range.includes('-')) {
33
+ const [start, end] = range.split('-').map(s => (s === '' ? null : parseFloat(s)));
34
+ if ((start !== null && isNaN(start)) || (end !== null && isNaN(end))) {
35
+ throw new Error(`Invalid value in range '${range}'`);
36
+ }
37
+ if (start !== null && end !== null && start > end) {
38
+ throw new Error(`Invalid range '${range}'`);
39
+ }
40
+ const compStart = start !== null ? `${valueName}${urlEncode ? encodeURIComponent(geOp) : geOp}${start}` : '';
41
+ const compEnd = end !== null ? `${valueName}${urlEncode ? encodeURIComponent(leOp) : leOp}${end}` : '';
42
+ const compRange = `${compStart}${compStart && compEnd ? ` ${andOp} ` : ''}${compEnd}`;
43
+ comparison += ranges.length > 1 ? `(${compRange})` : compRange;
44
+ } else {
45
+ const value = parseFloat(range);
46
+ if (isNaN(value)) {
47
+ throw new Error(`Invalid value '${range}'`);
48
+ }
49
+ const compValue = `${valueName}${urlEncode ? encodeURIComponent(eqOp) : eqOp}${value}`;
50
+ comparison += ranges.length > 1 ? `(${compValue})` : compValue;
51
+ }
52
+ if (i < ranges.length - 1) {
53
+ comparison += ` ${orOp} `;
54
+ }
55
+ }
56
+ return comparison;
57
+ }
@@ -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.29.0");
145
+ monsterVersion = new Version("3.31.0");
146
146
 
147
147
  return monsterVersion;
148
148
  }
@@ -0,0 +1,85 @@
1
+ import {
2
+ findElementWithIdUpwards
3
+ } from "../../../../application/source/dom/util.mjs";
4
+
5
+ import { expect } from 'chai';
6
+ import { JSDOM } from 'jsdom';
7
+
8
+ let originalEnvironment;
9
+
10
+ function setupTestEnvironment() {
11
+ const { window } = new JSDOM('<!DOCTYPE html>', { pretendToBeVisual: true });
12
+
13
+ const { document, customElements, HTMLElement } = window;
14
+ originalEnvironment = {
15
+ document: globalThis.document,
16
+ customElements: globalThis.customElements,
17
+ HTMLElement: globalThis.HTMLElement,
18
+ ShadowRoot: globalThis.ShadowRoot,
19
+ };
20
+ globalThis.document = document;
21
+ globalThis.customElements = customElements;
22
+ globalThis.HTMLElement = HTMLElement;
23
+ globalThis.ShadowRoot = window.ShadowRoot || class ShadowRoot {}; // Fallback for JSDOM
24
+
25
+ class TestComponent extends HTMLElement {
26
+ constructor() {
27
+ super();
28
+ this.attachShadow({ mode: 'open' });
29
+ }
30
+ }
31
+
32
+ if (!customElements.get('test-component')) {
33
+ customElements.define('test-component', TestComponent);
34
+ }
35
+ }
36
+
37
+ function cleanupTestEnvironment() {
38
+ Object.assign(globalThis, originalEnvironment);
39
+ }
40
+
41
+ describe('findElementWithIdUpwards', () => {
42
+ before(() => {
43
+ setupTestEnvironment();
44
+ });
45
+
46
+ after(() => {
47
+ cleanupTestEnvironment();
48
+ });
49
+
50
+ beforeEach(() => {
51
+ // Set up the DOM
52
+ document.body.innerHTML = `
53
+ <div id="container">
54
+ <div id="parent">
55
+ <div id="child"></div>
56
+ </div>
57
+ </div>
58
+ `;
59
+
60
+ const shadowHost = document.createElement('div');
61
+ document.body.appendChild(shadowHost);
62
+ const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
63
+ const innerElement = document.createElement('div');
64
+ innerElement.id = 'inner';
65
+ shadowRoot.appendChild(innerElement);
66
+ });
67
+
68
+ it('should find the element with the target ID in the normal DOM', () => {
69
+ const child = document.getElementById('child');
70
+ const result = findElementWithIdUpwards(child, 'parent');
71
+ expect(result).to.equal(document.getElementById('parent'));
72
+ });
73
+
74
+ it('should find the element with the target ID in the shadow DOM', () => {
75
+ const innerElement = document.querySelector('div[shadowroot] > div');
76
+ const result = findElementWithIdUpwards(innerElement, 'inner');
77
+ expect(result).to.equal(innerElement);
78
+ });
79
+
80
+ it('should return null if the element with the target ID is not found', () => {
81
+ const child = document.getElementById('child');
82
+ const result = findElementWithIdUpwards(child, 'nonexistent');
83
+ expect(result).to.be.null;
84
+ });
85
+ });
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  import {
4
2
  getDocument, getWindow, getDocumentFragmentFromString
5
3
  } from "../../../../application/source/dom/util.mjs";
@@ -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.29.0")
10
+ monsterVersion = new Version("3.31.0")
11
11
 
12
12
  let m = getMonsterVersion();
13
13
 
@@ -204,4 +204,84 @@ describe('Formatter', function () {
204
204
  });
205
205
 
206
206
 
207
+
208
+
209
+ describe('Formatter', () => {
210
+ it('should format a basic string with object values', () => {
211
+ const formatter = new Formatter({name: 'John', age: 30});
212
+ const result = formatter.format('My name is ${name} and I am ${age | tostring} years old.');
213
+
214
+ expect(result).to.equal('My name is John and I am 30 years old.');
215
+ });
216
+
217
+ it('should format a string with nested markers', () => {
218
+ const text = '${mykey${subkey}}';
219
+ const obj = {mykey2: '1', subkey: '2'};
220
+ const formatter = new Formatter(obj);
221
+
222
+ expect(formatter.format(text)).to.equal('1');
223
+ });
224
+
225
+ it('should format a string with custom markers', () => {
226
+ const formatter = new Formatter({name: 'John', age: 30});
227
+ formatter.setMarker('[', ']');
228
+ const result = formatter.format('My name is [name] and I am [age | tostring] years old.');
229
+
230
+ expect(result).to.equal('My name is John and I am 30 years old.');
231
+ });
232
+
233
+ it('should format a string using callback', () => {
234
+ const formatter = new Formatter({x: '1'}, {
235
+ callbacks: {
236
+ quote: (value) => {
237
+ return '"' + value + '"';
238
+ },
239
+ },
240
+ });
241
+
242
+ expect(formatter.format('${x | call:quote}')).to.equal('"1"');
243
+ });
244
+
245
+ it('should format a string with parameters', () => {
246
+ const obj = {
247
+ a: {
248
+ b: {
249
+ c: 'Hello',
250
+ },
251
+ d: 'world',
252
+ },
253
+ };
254
+ const formatter = new Formatter(obj);
255
+ const result = formatter.format('${a.b.c} ${a.d | ucfirst}!');
256
+
257
+ expect(result).to.equal('Hello World!');
258
+ });
259
+
260
+ it('should throw a too deep nesting error', () => {
261
+ const formatter = new Formatter({name: 'John'});
262
+ const nestedText = '${name${name${name${name${name${name${name${name${name${name${name${name${name${name${name${name${name${name${name}}}}}}}}}}}}}}}}}}';
263
+ expect(() => formatter.format(nestedText)).to.throw('syntax error in formatter template');
264
+ });
265
+
266
+ it('should throw a too deep nesting error', () => {
267
+ const inputObj = {
268
+ mykey: '${mykey}',
269
+ };
270
+
271
+ const formatter = new Formatter(inputObj);
272
+
273
+ const text = '${mykey}';
274
+ let formattedText = text;
275
+
276
+ // Create a string with 21 levels of nesting
277
+ for (let i = 0; i < 21; i++) {
278
+ formattedText = '${' + formattedText + '}';
279
+ }
280
+
281
+ expect(() => formatter.format(formattedText)).to.throw('too deep nesting');
282
+ });
283
+
284
+ });
285
+
286
+
207
287
  });
@@ -0,0 +1,109 @@
1
+ import {expect} from "chai"
2
+ import {generateRangeComparisonExpression} from "../../../../application/source/text/util.mjs";
3
+
4
+ describe('generateRangeComparisonExpression', () => {
5
+ it('should generate correct comparison expression for single values', () => {
6
+ const expression = '1,3,5';
7
+ const valueName = 'x';
8
+ const result = generateRangeComparisonExpression(expression, valueName);
9
+ expect(result).to.equal('(x==1) || (x==3) || (x==5)');
10
+ });
11
+
12
+ it('should generate correct comparison expression for ranges', () => {
13
+ const expression = '1-3,6-8';
14
+ const valueName = 'x';
15
+ const result = generateRangeComparisonExpression(expression, valueName);
16
+ expect(result).to.equal('(x>=1 && x<=3) || (x>=6 && x<=8)');
17
+ });
18
+
19
+ it('should generate correct comparison expression for mixed ranges and single values', () => {
20
+ const expression = '1-3,5,7-9';
21
+ const valueName = 'x';
22
+ const result = generateRangeComparisonExpression(expression, valueName);
23
+ expect(result).to.equal('(x>=1 && x<=3) || (x==5) || (x>=7 && x<=9)');
24
+ });
25
+
26
+ it('should throw an error for invalid range', () => {
27
+ const expression = '1-3,5-4';
28
+ const valueName = 'x';
29
+ expect(() => generateRangeComparisonExpression(expression, valueName)).to.throw(`Invalid range '5-4'`);
30
+ });
31
+
32
+
33
+ it('should throw an error for invalid value', () => {
34
+ const expression = '1-3,a';
35
+ const valueName = 'x';
36
+ expect(() => generateRangeComparisonExpression(expression, valueName)).to.throw('Invalid value');
37
+ });
38
+
39
+ it('should generate correct comparison expression with custom operators', () => {
40
+ const expression = '1-3,5';
41
+ const valueName = 'x';
42
+ const options = {
43
+ andOp: 'AND',
44
+ orOp: 'OR',
45
+ eqOp: '===',
46
+ geOp: '>=',
47
+ leOp: '<=',
48
+ };
49
+ const result = generateRangeComparisonExpression(expression, valueName, options);
50
+ expect(result).to.equal('(x>=1 AND x<=3) OR (x===5)');
51
+ });
52
+
53
+ it('should generate correct comparison expression with urlEncode option', () => {
54
+ const testCases = [
55
+ {
56
+ expression: '1,3,5',
57
+ valueName: 'x',
58
+ expected: '(x%3D%3D1) || (x%3D%3D3) || (x%3D%3D5)',
59
+ },
60
+ {
61
+ expression: '-10',
62
+ valueName: 'x',
63
+ expected: 'x%3C%3D10',
64
+ },
65
+ {
66
+ expression: '10-',
67
+ valueName: 'x',
68
+ expected: 'x%3E%3D10',
69
+ },
70
+ {
71
+ expression: '1-3,6-8',
72
+ valueName: 'y',
73
+ expected: '(y%3E%3D1 && y%3C%3D3) || (y%3E%3D6 && y%3C%3D8)',
74
+ },
75
+ {
76
+ expression: '1-3,5,7-9',
77
+ valueName: 'z',
78
+ expected: '(z%3E%3D1 && z%3C%3D3) || (z%3D%3D5) || (z%3E%3D7 && z%3C%3D9)',
79
+ },
80
+ ];
81
+
82
+ testCases.forEach(({expression, valueName, expected}) => {
83
+ const result = generateRangeComparisonExpression(expression, valueName, {urlEncode: true});
84
+ expect(result).to.equal(expected);
85
+ });
86
+ });
87
+
88
+ it('should generate correct comparison expression for open-ended ranges with urlEncode option', () => {
89
+ const testCases = [
90
+ {
91
+ expression: '10-',
92
+ valueName: 'x',
93
+ expected: 'x%3E%3D10',
94
+ },
95
+ {
96
+ expression: '-10',
97
+ valueName: 'y',
98
+ expected: 'y%3C%3D10',
99
+ },
100
+ ];
101
+
102
+ testCases.forEach(({expression, valueName, expected}) => {
103
+ const result = generateRangeComparisonExpression(expression, valueName, {urlEncode: true});
104
+ expect(result).to.equal(expected);
105
+ });
106
+ });
107
+
108
+
109
+ });