@schukai/monster 3.29.0 → 3.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/source/dom/util.mjs +50 -1
- package/source/text/util.mjs +57 -0
- package/source/types/version.mjs +1 -1
- package/test/cases/dom/find.mjs +85 -0
- package/test/cases/dom/util.mjs +0 -2
- package/test/cases/monster.mjs +1 -1
- package/test/cases/text/formatter.mjs +80 -0
- package/test/cases/text/util.mjs +109 -0
package/package.json
CHANGED
package/source/dom/util.mjs
CHANGED
|
@@ -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
|
+
}
|
package/source/types/version.mjs
CHANGED
|
@@ -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
|
+
});
|
package/test/cases/dom/util.mjs
CHANGED
package/test/cases/monster.mjs
CHANGED
|
@@ -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
|
+
});
|