@schukai/monster 4.91.1 → 4.93.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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
## [4.93.0] - 2026-01-13
|
|
6
|
+
|
|
7
|
+
### Add Features
|
|
8
|
+
|
|
9
|
+
- Enhance formatter with recursion detection and cycle handling
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## [4.92.0] - 2026-01-12
|
|
14
|
+
|
|
15
|
+
### Add Features
|
|
16
|
+
|
|
17
|
+
- Improve data resolution in form handling [#372](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/372)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
5
21
|
## [4.91.1] - 2026-01-12
|
|
6
22
|
|
|
7
23
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.
|
|
1
|
+
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.93.0"}
|
|
@@ -1043,34 +1043,32 @@ function syncOptionData() {
|
|
|
1043
1043
|
* @return {object}
|
|
1044
1044
|
*/
|
|
1045
1045
|
function resolveFormRecord(form, datasource) {
|
|
1046
|
-
|
|
1046
|
+
const { data, dataPath } = resolveDatasourceBasePath(form, datasource);
|
|
1047
1047
|
if (!isObject(data) && !isArray(data)) {
|
|
1048
1048
|
return {};
|
|
1049
1049
|
}
|
|
1050
1050
|
|
|
1051
|
-
|
|
1052
|
-
if (
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1051
|
+
let resolvedData = data;
|
|
1052
|
+
if (isObject(resolvedData) && !isArray(resolvedData)) {
|
|
1053
|
+
if (isArray(resolvedData.data)) {
|
|
1054
|
+
resolvedData = resolvedData.data;
|
|
1055
|
+
} else if (isArray(resolvedData.dataset)) {
|
|
1056
|
+
resolvedData = resolvedData.dataset;
|
|
1057
|
+
} else if (dataPath === "") {
|
|
1058
|
+
resolvedData = Object.values(resolvedData);
|
|
1057
1059
|
}
|
|
1058
1060
|
}
|
|
1059
1061
|
|
|
1060
|
-
if (isObject(data)) {
|
|
1061
|
-
data = Object.values(data);
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
1062
|
const mappingIndex = form.getOption?.("mapping.index");
|
|
1065
1063
|
if (mappingIndex !== null && mappingIndex !== undefined) {
|
|
1066
|
-
|
|
1064
|
+
resolvedData = resolvedData?.[mappingIndex];
|
|
1067
1065
|
}
|
|
1068
1066
|
|
|
1069
|
-
if (!isObject(
|
|
1067
|
+
if (!isObject(resolvedData) && !isArray(resolvedData)) {
|
|
1070
1068
|
return {};
|
|
1071
1069
|
}
|
|
1072
1070
|
|
|
1073
|
-
return
|
|
1071
|
+
return resolvedData;
|
|
1074
1072
|
}
|
|
1075
1073
|
|
|
1076
1074
|
/**
|
|
@@ -1085,14 +1083,14 @@ function getDatasourceContext() {
|
|
|
1085
1083
|
return null;
|
|
1086
1084
|
}
|
|
1087
1085
|
|
|
1088
|
-
const mappingData = form?.getOption?.("mapping.data");
|
|
1089
1086
|
const mappingIndex = form?.getOption?.("mapping.index");
|
|
1087
|
+
const { dataPath } = resolveDatasourceBasePath(form, datasource);
|
|
1090
1088
|
const record = resolveFormRecord(form, datasource);
|
|
1091
1089
|
const normalizedPath = normalizePathForRecord(path, record);
|
|
1092
1090
|
let basePath = "";
|
|
1093
1091
|
|
|
1094
|
-
if (isString(
|
|
1095
|
-
basePath =
|
|
1092
|
+
if (isString(dataPath) && dataPath.trim() !== "") {
|
|
1093
|
+
basePath = dataPath.trim();
|
|
1096
1094
|
}
|
|
1097
1095
|
|
|
1098
1096
|
if (mappingIndex !== null && mappingIndex !== undefined) {
|
|
@@ -1104,6 +1102,42 @@ function getDatasourceContext() {
|
|
|
1104
1102
|
return { target: datasource, path: finalPath };
|
|
1105
1103
|
}
|
|
1106
1104
|
|
|
1105
|
+
/**
|
|
1106
|
+
* @private
|
|
1107
|
+
* @param {HTMLElement} form
|
|
1108
|
+
* @param {HTMLElement} datasource
|
|
1109
|
+
* @return {{data: object|Array, dataPath: string}}
|
|
1110
|
+
*/
|
|
1111
|
+
function resolveDatasourceBasePath(form, datasource) {
|
|
1112
|
+
let data = datasource?.data;
|
|
1113
|
+
if (!isObject(data) && !isArray(data)) {
|
|
1114
|
+
return { data: {}, dataPath: "" };
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
let dataPath = "";
|
|
1118
|
+
const mappingData = form?.getOption?.("mapping.data");
|
|
1119
|
+
if (isString(mappingData) && mappingData.trim() !== "") {
|
|
1120
|
+
const trimmed = mappingData.trim();
|
|
1121
|
+
try {
|
|
1122
|
+
const mapped = new Pathfinder(data).getVia(trimmed);
|
|
1123
|
+
if (mapped !== undefined) {
|
|
1124
|
+
data = mapped;
|
|
1125
|
+
dataPath = trimmed;
|
|
1126
|
+
}
|
|
1127
|
+
} catch {}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (dataPath === "" && isObject(data) && !isArray(data)) {
|
|
1131
|
+
if (isArray(data.data)) {
|
|
1132
|
+
dataPath = "data";
|
|
1133
|
+
} else if (isArray(data.dataset)) {
|
|
1134
|
+
dataPath = "dataset";
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
return { data, dataPath };
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1107
1141
|
/**
|
|
1108
1142
|
* @private
|
|
1109
1143
|
* @return {string}
|
|
@@ -34,6 +34,12 @@ const internalObjectSymbol = Symbol("internalObject");
|
|
|
34
34
|
*/
|
|
35
35
|
const watchdogSymbol = Symbol("watchdog");
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* @private
|
|
39
|
+
* @type {symbol}
|
|
40
|
+
*/
|
|
41
|
+
const recursionStackSymbol = Symbol("recursionStack");
|
|
42
|
+
|
|
37
43
|
/**
|
|
38
44
|
* @private
|
|
39
45
|
* @type {symbol}
|
|
@@ -145,6 +151,10 @@ class Formatter extends BaseWithOptions {
|
|
|
145
151
|
open: ["${"],
|
|
146
152
|
close: ["}"],
|
|
147
153
|
},
|
|
154
|
+
recursion: {
|
|
155
|
+
maxDepth: 20,
|
|
156
|
+
detectCycles: true,
|
|
157
|
+
},
|
|
148
158
|
parameter: {
|
|
149
159
|
delimiter: "::",
|
|
150
160
|
assignment: "=",
|
|
@@ -226,6 +236,7 @@ class Formatter extends BaseWithOptions {
|
|
|
226
236
|
this[markerOpenIndexSymbol] = 0;
|
|
227
237
|
this[markerCloseIndexSymbol] = 0;
|
|
228
238
|
this[workingDataSymbol] = {};
|
|
239
|
+
this[recursionStackSymbol] = [];
|
|
229
240
|
return format.call(this, text);
|
|
230
241
|
}
|
|
231
242
|
}
|
|
@@ -236,7 +247,14 @@ class Formatter extends BaseWithOptions {
|
|
|
236
247
|
*/
|
|
237
248
|
function format(text) {
|
|
238
249
|
this[watchdogSymbol]++;
|
|
239
|
-
|
|
250
|
+
const recursionOptions = this[internalSymbol]["recursion"] || {};
|
|
251
|
+
const maxDepth =
|
|
252
|
+
typeof recursionOptions.maxDepth === "number"
|
|
253
|
+
? recursionOptions.maxDepth
|
|
254
|
+
: 20;
|
|
255
|
+
const detectCycles = recursionOptions.detectCycles !== false;
|
|
256
|
+
|
|
257
|
+
if (this[watchdogSymbol] > maxDepth) {
|
|
240
258
|
throw new Error("too deep nesting");
|
|
241
259
|
}
|
|
242
260
|
|
|
@@ -252,13 +270,22 @@ function format(text) {
|
|
|
252
270
|
return text;
|
|
253
271
|
}
|
|
254
272
|
|
|
255
|
-
|
|
273
|
+
const { text: tokenizedText, keys } = tokenize.call(
|
|
256
274
|
this,
|
|
257
275
|
validateString(text),
|
|
258
276
|
openMarker,
|
|
259
277
|
closeMarker,
|
|
278
|
+
detectCycles,
|
|
260
279
|
);
|
|
261
280
|
|
|
281
|
+
let pushedKeys = 0;
|
|
282
|
+
if (detectCycles && keys.length > 0) {
|
|
283
|
+
for (const key of keys) {
|
|
284
|
+
this[recursionStackSymbol].push(key);
|
|
285
|
+
}
|
|
286
|
+
pushedKeys = keys.length;
|
|
287
|
+
}
|
|
288
|
+
|
|
262
289
|
if (
|
|
263
290
|
this[internalSymbol]["marker"]["open"]?.[this[markerOpenIndexSymbol] + 1]
|
|
264
291
|
) {
|
|
@@ -271,7 +298,14 @@ function format(text) {
|
|
|
271
298
|
this[markerCloseIndexSymbol]++;
|
|
272
299
|
}
|
|
273
300
|
|
|
274
|
-
|
|
301
|
+
let result;
|
|
302
|
+
try {
|
|
303
|
+
result = format.call(this, tokenizedText);
|
|
304
|
+
} finally {
|
|
305
|
+
if (pushedKeys > 0) {
|
|
306
|
+
this[recursionStackSymbol].splice(-pushedKeys);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
275
309
|
|
|
276
310
|
return result;
|
|
277
311
|
}
|
|
@@ -286,12 +320,14 @@ function format(text) {
|
|
|
286
320
|
* @param {string} closeMarker
|
|
287
321
|
* @return {string}
|
|
288
322
|
*/
|
|
289
|
-
function tokenize(text, openMarker, closeMarker) {
|
|
323
|
+
function tokenize(text, openMarker, closeMarker, detectCycles) {
|
|
290
324
|
const formatted = [];
|
|
325
|
+
const keys = [];
|
|
291
326
|
|
|
292
327
|
const parameterAssignment = this[internalSymbol]["parameter"]["assignment"];
|
|
293
328
|
const parameterDelimiter = this[internalSymbol]["parameter"]["delimiter"];
|
|
294
329
|
const callbacks = this[internalSymbol]["callbacks"];
|
|
330
|
+
const activeKeys = this[recursionStackSymbol] || [];
|
|
295
331
|
|
|
296
332
|
while (true) {
|
|
297
333
|
const startIndex = text.indexOf(openMarker);
|
|
@@ -317,8 +353,16 @@ function tokenize(text, openMarker, closeMarker) {
|
|
|
317
353
|
text.substring(insideStartIndex),
|
|
318
354
|
openMarker,
|
|
319
355
|
closeMarker,
|
|
356
|
+
detectCycles,
|
|
320
357
|
);
|
|
321
|
-
text = text.substring(0, insideStartIndex) + result;
|
|
358
|
+
text = text.substring(0, insideStartIndex) + result.text;
|
|
359
|
+
if (detectCycles && result.keys?.length) {
|
|
360
|
+
for (const key of result.keys) {
|
|
361
|
+
if (!keys.includes(key)) {
|
|
362
|
+
keys.push(key);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
322
366
|
endIndex = text.substring(openMarker.length).indexOf(closeMarker);
|
|
323
367
|
if (endIndex !== -1) endIndex += openMarker.length;
|
|
324
368
|
}
|
|
@@ -331,6 +375,16 @@ function tokenize(text, openMarker, closeMarker) {
|
|
|
331
375
|
const key = text.substring(openMarker.length, endIndex);
|
|
332
376
|
const parts = key.split(parameterDelimiter);
|
|
333
377
|
const currentPipe = parts.shift();
|
|
378
|
+
const recursionKey = getRecursionKey(currentPipe);
|
|
379
|
+
|
|
380
|
+
if (detectCycles && recursionKey) {
|
|
381
|
+
if (activeKeys.includes(recursionKey)) {
|
|
382
|
+
throw new Error("cycle detected in formatter");
|
|
383
|
+
}
|
|
384
|
+
if (!keys.includes(recursionKey)) {
|
|
385
|
+
keys.push(recursionKey);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
334
388
|
|
|
335
389
|
this[workingDataSymbol] = extend(
|
|
336
390
|
{},
|
|
@@ -372,5 +426,20 @@ function tokenize(text, openMarker, closeMarker) {
|
|
|
372
426
|
text = text.substring(endIndex + closeMarker.length);
|
|
373
427
|
}
|
|
374
428
|
|
|
375
|
-
return formatted.join("");
|
|
429
|
+
return { text: formatted.join(""), keys };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* @private
|
|
434
|
+
* @param {string} pipe
|
|
435
|
+
* @return {string|null}
|
|
436
|
+
*/
|
|
437
|
+
function getRecursionKey(pipe) {
|
|
438
|
+
if (!pipe) return null;
|
|
439
|
+
const rawKey = pipe.split("|").shift().trim();
|
|
440
|
+
if (rawKey === "") return null;
|
|
441
|
+
if (rawKey.indexOf("path:") === 0) return rawKey.substring("path:".length);
|
|
442
|
+
if (rawKey.indexOf("static:") === 0)
|
|
443
|
+
return rawKey.substring("static:".length);
|
|
444
|
+
return rawKey;
|
|
376
445
|
}
|
|
@@ -288,7 +288,11 @@ describe('Formatter', function () {
|
|
|
288
288
|
mykey: '${mykey}',
|
|
289
289
|
};
|
|
290
290
|
|
|
291
|
-
const formatter = new Formatter(inputObj
|
|
291
|
+
const formatter = new Formatter(inputObj, {
|
|
292
|
+
recursion: {
|
|
293
|
+
detectCycles: false,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
292
296
|
|
|
293
297
|
const text = '${mykey}';
|
|
294
298
|
let formattedText = text;
|
|
@@ -300,8 +304,28 @@ describe('Formatter', function () {
|
|
|
300
304
|
|
|
301
305
|
expect(() => formatter.format(formattedText)).to.throw('too deep nesting');
|
|
302
306
|
});
|
|
307
|
+
|
|
308
|
+
it('should detect recursion cycles by default', () => {
|
|
309
|
+
const formatter = new Formatter({
|
|
310
|
+
a: '${b}',
|
|
311
|
+
b: '${a}',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(() => formatter.format('${a}')).to.throw('cycle detected in formatter');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should allow configuring max depth', () => {
|
|
318
|
+
const formatter = new Formatter({value: '${value}'}, {
|
|
319
|
+
recursion: {
|
|
320
|
+
maxDepth: 3,
|
|
321
|
+
detectCycles: false,
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(() => formatter.format('${value}')).to.throw('too deep nesting');
|
|
326
|
+
});
|
|
303
327
|
|
|
304
328
|
});
|
|
305
329
|
|
|
306
330
|
|
|
307
|
-
});
|
|
331
|
+
});
|