@schukai/monster 4.92.0 → 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,14 @@
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
+
5
13
  ## [4.92.0] - 2026-01-12
6
14
 
7
15
  ### Add Features
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.92.0"}
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"}
@@ -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
- if (this[watchdogSymbol] > 20) {
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
- let result = tokenize.call(
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
- result = format.call(this, result);
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
+ });