cavalion-js 1.0.84 → 1.0.85

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
@@ -1,3 +1,53 @@
1
+ ### `2026/03/16` 1.0.85 Formatting and entity expansion updates
2
+
3
+ **console/node/vcl/Component.js**:
4
+
5
+ * Adds root-component console rendering with `uri#hashCode`
6
+ * Keeps non-root `Component` labels unchanged; still shows `:root` and `:selected` markers.
7
+ * **Breaking**: code using `expand.Entity(...)` must migrate to `expand.newEntity(...)`.
8
+
9
+ **entities/expand.js**:
10
+
11
+ * Renames `expand.Entity` to `expand.newEntity` in
12
+ * Adds `expand.DefaultEntity` for generic alias-based `join()` and `expand()` behavior.
13
+ * Improves `expand.attributes4()` fallback handling for unknown entity relations and single-item tuple paths.
14
+ * Extends `prefixId()` to preserve `count:id` selectors without prepending `id,`.
15
+
16
+ **js/_js.js**:
17
+
18
+ * Adds `js.qq(a, b)` for nullish fallback values.
19
+ * Fixes `String.format()` width and padding behavior for `%d`, including negative zero-padded numbers.
20
+ * Fixes `String.format()` float formatting for `%f`, including width, precision, and negative values.
21
+ * Fixes string alignment in `String.format()` for `%s`/`%H`/`%n`; left/right padding now behaves consistently.
22
+ * Improves `%` specifier parsing to stop safely at end-of-format input.
23
+ * Adjusts numeric formatting to return the original positive value when fixed `"0000"` formatting collapses to `"0"`.
24
+
25
+ **locale.js**:
26
+
27
+ * Adds `locale.prefixed(...).prefixed(...)` chaining in `src/locale.js`.
28
+ * Fixes prefixed locale fallback output so missing keys do not duplicate the prefix.
29
+
30
+
31
+ ### `2026/02/05` Introducing entities.expand
32
+
33
+ It turns `expand.js` from a small “string builder” helper into a mini expansion/join framework.
34
+
35
+ * **Before:** the module essentially returned a function that, given an attribute (or list of attributes) plus a `path`, produced strings like `path.attr`, with some support for comma lists, aliases (`"id foo"`), and typed prefixes (`"Type:attr"`). Arrays were turned into a **comma-joined string**.
36
+
37
+ * **Now:** it exports a reusable `expand(expanderFn, as, path)` function plus an **`Entity` factory** (`expand.Entity(...)`) that builds an object representing an entity with:
38
+
39
+ * **Named expanders** generated from a `paths` map (so you can call `Entity.bedrijf("id")` and get `meetpunt.onderzoek.bedrijf.id`).
40
+ * A **`join(alias, attributes)`** method that resolves a join alias (case-insensitive) to one of those expanders via a mapping; unknown aliases now **throw an error**.
41
+ * An **`expand(...)`** convenience method that expands multiple attributes using `expand.attributes4(...)`.
42
+
43
+ * **Normalization changes:**
44
+
45
+ * Comma-separated strings are split and **trimmed**.
46
+ * If `as` is an array, it maps each item through the expander and returns an **array** (not a comma-joined string).
47
+ * There’s a helper that **auto-prefixes `"id"`** (e.g., ensures `"id"` is included in expansions) and supports mixed input shapes (strings and `[expanderName, attrs]` tuples).
48
+
49
+ Overall, it standardizes how “expand fields” and “join expansions” are defined and executed, moving from ad-hoc string concatenation to an entity-centric API with explicit alias resolution and stricter error handling.
50
+
1
51
  ### `2026/01/18` - 1.0.84
2
52
 
3
53
  EM now uses the browser’s fetch API instead of jQuery.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cavalion-js",
3
- "version": "1.0.84",
3
+ "version": "1.0.85",
4
4
  "description": "Cavalion common JavaScript library",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -16,17 +16,25 @@ define(function(require) {
16
16
  /** @overrides ../Node.prototype.initializeValue */
17
17
  // node.innerHTML = String.format("%H<span class='uri'> - %H</span>",
18
18
  // js.nameOf(this._value), this._value.getUri());
19
- var root = this._value.isRootComponent() ? ":root" : "";
19
+ var isRoot = this._value.isRootComponent();
20
+ var root = isRoot ? ":root" : "";
20
21
  var uri = this._value._uri;//this._value.isRootComponent() ? this._value._uri : this._value.getUri();
21
22
  if(uri !== this._value.getUri()) {
22
23
  uri = js.sf("%s - %s", uri, this._value.getUri());
23
24
  }
24
-
25
+
25
26
  var selected = this._value.isSelected && this._value.isSelected() ? ":selected" : "";
26
- node.innerHTML = String.format(
27
- "%H<span class='uri'> - %H%H%H</span>",
28
- js.nameOf(this._value),
29
- uri, root, selected);
27
+ if(isRoot) {
28
+ node.innerHTML = String.format(
29
+ "%H#%s<span class='uri'> - %H%H%H</span>",
30
+ uri, this._value.hashCode(), js.nameOf(this._value),
31
+ root, selected);
32
+ } else {
33
+ node.innerHTML = String.format(
34
+ "%H<span class='uri'> - %H%H%H</span>",
35
+ js.nameOf(this._value),
36
+ uri, root, selected);
37
+ }
30
38
  }
31
39
  },
32
40
  statics: {
@@ -1,30 +1,146 @@
1
- define([], () => (fn, as, path) => {
2
- if(as === undefined) {
3
- return path;
4
- }
5
- if(typeof as === "string" && as.indexOf(",") !== -1) {
6
- as = as.split(",");
7
- }
1
+ define(["js"], (js) => {
8
2
 
9
- if(as instanceof Array) {
10
- return as.map(fn).join(",");
11
- }
3
+ // TODO join and expand => @join, @expand
12
4
 
13
- if(path) {
14
- let alias = "";
5
+ const KEY_name = "@_name", KEY_expanders = "@_expanders";
6
+
7
+ function Entity(name) {
8
+ this[KEY_name] = name;
9
+ this[KEY_expanders] = {};
10
+ }
11
+ Entity.prototype.join = function(alias, attributes) {
12
+ const expanders = this[KEY_expanders];
13
+ const expander = expanders[alias] || expanders[alias.toLowerCase()];
15
14
 
16
- as = as.split(" ");
17
- if(as.length === 2) {
18
- alias = " " + as.pop();
15
+ if(!expander) {
16
+ throw new Error("Unknown join alias/entity: " + alias);
19
17
  }
20
- as = as.pop();
21
18
 
22
- as = as.split(":");
23
- if(as.length > 1) {
24
- return js.sf("%s:%s.%s%s", as[0], path, as[1], alias);
19
+ return expander(attributes);
20
+ };
21
+ Entity.prototype.expand = function(...args) {
22
+ const arr = (args.length === 1 && Array.isArray(args[0])) ? args[0] : args;
23
+ return expand.attributes4(this, arr);
24
+ };
25
+
26
+ const isFn = (f) => typeof f === "function";
27
+ const makeFn = (path) => {
28
+ /**
29
+ * makeFn(path) -> function(as) { ... }
30
+ * Creates a self-recursive expander that calls expand(fn, as, path).
31
+ */
32
+ function fn(as) {
33
+ return expand(fn, as, path);
34
+ }
35
+ return fn;
36
+ };
37
+ const prefixId = (s) => !s ? "id" : s.startsWith("id") || s.startsWith("count:id") ? s : "id," + s;
38
+
39
+ function expand(expander, as, path) {
40
+ /**
41
+ * expand(expanderFn, as, path)
42
+ *
43
+ * - If as === undefined: returns path (so expanders can self-describe)
44
+ * - If as is "a,b,c": treated as ["a","b","c"]
45
+ * - If as is Array: maps each element through the same expander
46
+ * - If as contains "attr alias": returns "path.attr alias"
47
+ * - If as contains "type:attr": returns "type:path.attr"
48
+ */
49
+ if(as === undefined) {
50
+ return path;
51
+ }
52
+ if(typeof as === "string" && as.indexOf(",") !== -1) {
53
+ as = as.split(",").map(function(s) { return s.trim(); });
54
+ }
55
+ if(Array.isArray(as)) {
56
+ return as.map(function(a) { return expander(a); });
57
+ }
58
+
59
+ var attr = String(as);
60
+
61
+ if(path) {
62
+ // typed prefix: "Watermonster:id" => "Watermonster:path.id"
63
+ if(attr.indexOf(":") !== -1) {
64
+ var t = attr.split(":");
65
+ return js.sf("%s:%s.%s", t[0], path, t[1]);
66
+ }
67
+
68
+ // aliasing: "id foo" => "path.id foo"
69
+ if(attr.indexOf(" ") !== -1) {
70
+ var parts = attr.split(" ");
71
+ return js.sf("%s.%s %s", path, parts[0], parts.slice(1).join(" "));
72
+ }
73
+
74
+
75
+ return js.sf("%s.%s", path, attr);
76
+ }
77
+
78
+ // No path: just return the attribute as-is (e.g. root filter fields)
79
+ return attr;
80
+ }
81
+ expand.newEntity = function(name, paths, joinToExpander) {
82
+ /**
83
+ * expand.entity(name, paths, joinToExpander)
84
+ *
85
+ * paths: { bedrijf: "meetpunt.onderzoek.bedrijf", ... }
86
+ * joinToExpander: { Watermonster: "watermonsters", ... }
87
+ *
88
+ * Returns an object where each expander is available directly as a method:
89
+ * Entity.bedrijf("id") -> "meetpunt.onderzoek.bedrijf.id"
90
+ *
91
+ * Also provides:
92
+ * Entity.join("Bedrijf", "id") -> same, via joinToExpander / expanders lookup
93
+ */
94
+ const entity = new Entity(name);
95
+
96
+ // Build expanders and attach them as direct methods (Entity.bedrijf(...))
97
+ for(const k in paths) {
98
+ if(paths.hasOwnProperty(k)) {
99
+ const fn = makeFn(paths[k] || undefined);
100
+ entity[KEY_expanders][k] = fn;
101
+ if(!entity[k]) entity[k] = fn;
102
+ }
103
+ }
104
+
105
+ // Normalize join map keys to lower-case
106
+ for(const j in joinToExpander) {
107
+ if(joinToExpander.hasOwnProperty(j) && !entity[KEY_expanders][j]) {
108
+ entity[KEY_expanders][j] = entity[KEY_expanders][joinToExpander[j]];
109
+ }
110
+ }
111
+
112
+ return entity;
113
+ };
114
+ expand.attributes4 = (entity, arr) => arr.map(a => {
115
+ if(a instanceof Array) {
116
+ if(!entity[a[0]]) {
117
+ if(a.length === 1) {
118
+ return a[0] + ".id";
119
+ }
120
+ return prefixId(a[1]).split(",").map(s => a[0] + "." + s);
121
+ }
122
+ return entity[a[0]](prefixId(a[1]));
123
+ } else if(typeof a === "string") {
124
+ return prefixId(a).split(",");
125
+ }
126
+ }).flat();
127
+
128
+ function DefaultEntity(name) {
129
+ this[KEY_name] = name;
130
+ this.join = (alias, attributes) => {
131
+ return attributes.split(",").map(a => [alias, a].join("."));
132
+ },
133
+ this.expand = (...args) => {
134
+ return args.map(a => {
135
+ if(a instanceof Array) {
136
+ const arr = prefixId(a[1]).split(",");
137
+ return arr.map(path => [a[0], path].join("."));
138
+ } else if(typeof a === "string") {
139
+ return prefixId(a).split(",");
140
+ }
141
+ });
25
142
  }
26
- return js.sf("%s.%s%s", path, as, alias);
27
143
  }
28
144
 
29
- return as;
145
+ return js.mi(expand, { DefaultEntity });
30
146
  });
package/src/js/_js.js CHANGED
@@ -49,6 +49,7 @@ define(function(require) {
49
49
  sj: serialize.serialize, //JSON.stringify,
50
50
  pj: serialize.deserialize, //JSON.parse,
51
51
  sf: String.format,
52
+ qq: (a, b) => (a === null || a === undefined) ? b : a,
52
53
  eval: global[tlc("EVAL")],
53
54
  nameOf: nameOf,
54
55
  defineClass: defineClass,
@@ -309,12 +309,12 @@ const stringify = (obj) => {
309
309
  */
310
310
  var s = [];
311
311
  var idx = -1, pos = 0;
312
- var i = 1;
312
+ var argi = 1;
313
313
  var specifiers = "cdfsHn";
314
-
314
+
315
315
  do {
316
316
  idx = fmt.indexOf("%", ++idx);
317
-
317
+
318
318
  if(idx !== -1) {
319
319
  if(fmt.charAt(idx + 1) === "%") {
320
320
  s.push(fmt.substring(pos, idx));
@@ -323,29 +323,45 @@ const stringify = (obj) => {
323
323
  pos = idx + 1;
324
324
  } else {
325
325
  s.push(fmt.substring(pos, idx));
326
- var mod = "", ch = fmt.charAt(idx + 1);
327
- while(specifiers.indexOf(ch) === -1) {
326
+
327
+ var mod = "";
328
+ var ch = fmt.charAt(idx + 1);
329
+
330
+ while(specifiers.indexOf(ch) === -1 && ch !== "") {
328
331
  mod += ch;
329
332
  idx++;
330
333
  ch = fmt.charAt(idx + 1);
331
334
  }
335
+
332
336
  if(ch === "c") {
333
337
  if(mod === "*") {
334
- var n = arguments[i++];
338
+ var n = arguments[argi++];
335
339
  while(n--) {
336
- s.push(arguments[i]);
340
+ s.push(arguments[argi]);
337
341
  }
338
- i++;
342
+ argi++;
339
343
  } else {
340
- s.push(arguments[i++]);
344
+ s.push(arguments[argi++]);
341
345
  }
346
+
342
347
  } else if(ch === "d") {
343
- var value = "" + parseInt(arguments[i++], 10);
348
+ var n = parseInt(arguments[argi++], 10);
349
+ var value = "" + n;
350
+
344
351
  if(mod.length) {
352
+ var len;
353
+
345
354
  if(mod.charAt(0) === "0") {
346
- var len = parseInt(mod.substring(1), 10) || 0;
347
- while(value.length < len) {
348
- value = "0" + value;
355
+ len = parseInt(mod.substring(1), 10) || 0;
356
+
357
+ if(value.charAt(0) === "-") {
358
+ while(value.length < len) {
359
+ value = "-0" + value.substring(1);
360
+ }
361
+ } else {
362
+ while(value.length < len) {
363
+ value = "0" + value;
364
+ }
349
365
  }
350
366
  } else {
351
367
  len = parseInt(mod, 10) || 0;
@@ -354,57 +370,98 @@ const stringify = (obj) => {
354
370
  }
355
371
  }
356
372
  }
373
+
357
374
  s.push(value);
375
+
358
376
  } else if(ch === "f") {
359
- mod = mod.split(".");
360
- if(mod.length === 2) {
361
- len = parseInt(mod[1], 10) || 0;
362
- value = arguments[i++];
363
- var i1 = String.format("%" + mod[0] + "d", value);
364
- var f = Math.abs(value - i1);
365
- f *= Math.pow(10, len + 1);
366
- f = "" + Math.round(Math.round(f) / 10);
367
- while(f.length < len) {
368
- f = "0" + f;
377
+ var parts = mod.split(".");
378
+ var raw = Number(arguments[argi++]);
379
+
380
+ if(parts.length === 2 && !isNaN(raw)) {
381
+ var widthSpec = parts[0];
382
+ var precision = parseInt(parts[1], 10);
383
+ if(isNaN(precision)) {
384
+ precision = 0;
385
+ }
386
+
387
+ var negative = raw < 0 || (raw === 0 && 1 / raw < 0);
388
+ var abs = Math.abs(raw);
389
+ var fixed = abs.toFixed(precision);
390
+ var out = negative ? "-" + fixed : fixed;
391
+
392
+ if(widthSpec.length) {
393
+ var width;
394
+
395
+ if(widthSpec.charAt(0) === "0") {
396
+ width = parseInt(widthSpec.substring(1), 10) || 0;
397
+
398
+ if(out.charAt(0) === "-") {
399
+ while(out.length < width) {
400
+ out = "-0" + out.substring(1);
401
+ }
402
+ } else {
403
+ while(out.length < width) {
404
+ out = "0" + out;
405
+ }
406
+ }
407
+ } else {
408
+ width = parseInt(widthSpec, 10) || 0;
409
+ while(out.length < width) {
410
+ out = " " + out;
411
+ }
412
+ }
369
413
  }
370
- s.push(i1 + "." + f);
371
- } else {
372
- s.push(arguments[i++]);
373
- }
414
+
415
+ s.push(out);
416
+ } else {
417
+ s.push(arguments[argi - 1]);
418
+ }
419
+
374
420
  } else if(ch === "s" || ch === "H" || ch === "n") {
421
+ var value;
422
+ var len;
423
+
375
424
  if(ch === "n") {
376
- value = String.of(arguments[i++]);
425
+ value = String.of(arguments[argi++]);
377
426
  } else {
378
- value = "" + arguments[i++];
427
+ value = "" + arguments[argi++];
379
428
  }
429
+
380
430
  if(mod.charAt(0) === "-") {
381
431
  len = parseInt(mod.substring(1), 10) || 0;
382
432
  while(value.length < len) {
383
- value = " " + value;
433
+ value += " ";
384
434
  }
385
435
  } else {
386
436
  len = parseInt(mod, 10) || 0;
387
437
  if(mod === "*") {
388
438
  len = parseInt(value, 10);
389
- value = "" + arguments[i++];
439
+ value = "" + arguments[argi++];
390
440
  }
391
441
  while(value.length < len) {
392
- value += " ";
442
+ value = " " + value;
393
443
  }
394
444
  }
445
+
395
446
  if(ch === "H" && value) {
396
447
  try {
397
448
  value = String.escapeHtml(value);
398
- } catch(e) { value = e.message; }
449
+ } catch(e) {
450
+ value = e.message;
451
+ }
399
452
  }
453
+
400
454
  s.push(value);
455
+
401
456
  } else {
402
- s.push(arguments[i++]);
457
+ s.push(arguments[argi++]);
403
458
  }
459
+
404
460
  pos = idx + 2;
405
461
  }
406
462
  }
407
463
  } while(idx !== -1);
464
+
408
465
  s.push(fmt.substring(pos));
409
466
  return s.join("");
410
467
  };
@@ -455,7 +512,9 @@ const stringify = (obj) => {
455
512
  n = parseFloat(n);
456
513
 
457
514
  if((i = r.indexOf("0000")) > dot) {
458
- return n.toFixed(i - dot - 1);
515
+ var t = n.toFixed(i - dot - 1);
516
+ if(t === "0" && n > 0) return n;
517
+ return t;
459
518
  }
460
519
  if((i = r.indexOf("9999")) > dot) {
461
520
  return n.toFixed(i - dot - 1);
package/src/locale.js CHANGED
@@ -117,7 +117,7 @@ define(function(require) {
117
117
  var arr = (window.locale.missing = (window.locale.missing || []));
118
118
  arr.push(id);
119
119
  // console.warn("undefined locale: " + id);
120
- r = "{" + id + "}";
120
+ r = "{" + id + "}";
121
121
  } else if(typeof r === "function" && r.name === "locale") {
122
122
  /* automagically call functions named locale */
123
123
  r = r.apply(this, arguments);
@@ -172,11 +172,18 @@ define(function(require) {
172
172
  } else {
173
173
  args[0] = [prefix, id];
174
174
  }
175
- return locale.apply(this, args);
175
+ const r = locale.apply(this, args);
176
+ if(typeof r === "string" && r.endsWith("}") && r.startsWith("{" + prefix)) {
177
+ return "{" + r.substring(prefix.length + 1);
178
+ }
179
+ return r;
176
180
  };
177
181
 
178
182
  prefixed.has = (id) => locale.has(prefix + id);
179
-
183
+ prefixed.prefixed = (prefix2) => {
184
+ return locale.prefixed(prefix + prefix2);
185
+ };
186
+
180
187
  return prefixed;
181
188
  };
182
189
  locale.define = function(prefix, defaults) {