@schukai/monster 4.153.0 → 4.154.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.
Files changed (52) hide show
  1. package/package.json +1 -1
  2. package/source/components/form/message-state-button.mjs +1 -1
  3. package/source/components/form/select-link.mjs +12 -1
  4. package/source/components/state/log.mjs +3 -7
  5. package/source/components/state/thread.mjs +3 -7
  6. package/source/components/time/day.mjs +0 -19
  7. package/source/components/time/month-calendar.mjs +5 -1
  8. package/source/data/buildmap.mjs +6 -5
  9. package/source/data/buildtree.mjs +0 -8
  10. package/source/data/datasource.mjs +3 -10
  11. package/source/data/diff.mjs +13 -12
  12. package/source/data/extend.mjs +23 -1
  13. package/source/data/pathfinder.mjs +228 -32
  14. package/source/data/transformer.mjs +2 -2
  15. package/source/dom/customcontrol.mjs +35 -9
  16. package/source/dom/customelement.mjs +293 -31
  17. package/source/dom/slotted.mjs +46 -31
  18. package/source/dom/template.mjs +4 -1
  19. package/source/dom/updater.mjs +57 -17
  20. package/source/types/base.mjs +7 -6
  21. package/source/types/basewithoptions.mjs +1 -8
  22. package/source/types/internal.mjs +4 -10
  23. package/source/types/node.mjs +1 -3
  24. package/source/types/observerlist.mjs +9 -17
  25. package/source/types/proxyobserver.mjs +51 -12
  26. package/source/types/tokenlist.mjs +13 -8
  27. package/source/util/clone.mjs +44 -14
  28. package/test/cases/browser-bundle.mjs +75 -0
  29. package/test/cases/components/time/month-calendar.mjs +115 -0
  30. package/test/cases/data/buildmap.mjs +14 -0
  31. package/test/cases/data/buildtree.mjs +49 -0
  32. package/test/cases/data/diff.mjs +86 -0
  33. package/test/cases/data/extend.mjs +52 -0
  34. package/test/cases/data/pathfinder-resolution-baseline.mjs +322 -0
  35. package/test/cases/data/pathfinder.mjs +163 -0
  36. package/test/cases/data/transformer.mjs +26 -0
  37. package/test/cases/dom/customcontrol.mjs +85 -0
  38. package/test/cases/dom/customelement-rebind-baseline.mjs +323 -0
  39. package/test/cases/dom/customelement-teardown.mjs +408 -0
  40. package/test/cases/dom/customelement.mjs +408 -0
  41. package/test/cases/dom/dimension.mjs +34 -10
  42. package/test/cases/dom/slotted-nodes.mjs +48 -1
  43. package/test/cases/dom/template.mjs +11 -0
  44. package/test/cases/dom/updater-remove.mjs +198 -0
  45. package/test/cases/dom/updater.mjs +118 -0
  46. package/test/cases/packaging.mjs +213 -0
  47. package/test/cases/types/base.mjs +50 -0
  48. package/test/cases/types/observerlist.mjs +103 -1
  49. package/test/cases/types/proxyobserver.mjs +189 -0
  50. package/test/cases/types/regex.mjs +4 -0
  51. package/test/cases/types/tokenlist.mjs +64 -1
  52. package/test/cases/util/clone.mjs +115 -0
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","files":["source","test/cases","test/util","CHANGELOG.md"],"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.153.0"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","exports":{".":"./source/monster.mjs","./CHANGELOG.md":"./CHANGELOG.md","./LICENSE":"./LICENSE","./README.md":"./README.md","./package.json":"./package.json","./source/*":"./source/*","./test/*":"./test/*"},"files":["source","test/cases","test/util","CHANGELOG.md"],"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"},"sideEffects":["./source/monster.mjs","./source/components/**","./source/i18n/**","./test/**"],"type":"module","version":"4.154.0"}
@@ -333,8 +333,8 @@ class MessageStateButton extends Popper {
333
333
  }
334
334
 
335
335
  disconnectedCallback() {
336
- clearAutoHideTimer.call(this);
337
336
  super.disconnectedCallback();
337
+ clearAutoHideTimer.call(this);
338
338
  }
339
339
 
340
340
  /**
@@ -67,11 +67,22 @@ class SelectLink extends CustomElement {
67
67
 
68
68
  [assembleMethodSymbol]() {
69
69
  super[assembleMethodSymbol]();
70
- bindSelects.call(this);
71
70
  return this;
72
71
  }
73
72
 
73
+ /**
74
+ * Binding belongs to the connect lifecycle, not to assembly: disconnecting
75
+ * unbinds, so a reconnect has to bind again. bindSelects() is idempotent.
76
+ *
77
+ * @inheritdoc
78
+ */
79
+ connectedCallback() {
80
+ super.connectedCallback();
81
+ bindSelects.call(this);
82
+ }
83
+
74
84
  disconnectedCallback() {
85
+ super.disconnectedCallback();
75
86
  unbindSelects.call(this);
76
87
  }
77
88
  }
@@ -136,13 +136,9 @@ class Log extends CustomElement {
136
136
  */
137
137
  getOption(path, defaultValue = undefined) {
138
138
  if (path === "entries" || path?.startsWith("entries.")) {
139
- try {
140
- return new Pathfinder({
141
- entries: this[entriesSymbol],
142
- }).getVia(path);
143
- } catch (e) {
144
- return defaultValue;
145
- }
139
+ return new Pathfinder({
140
+ entries: this[entriesSymbol],
141
+ }).getVia(path, defaultValue);
146
142
  }
147
143
 
148
144
  return super.getOption(path, defaultValue);
@@ -143,13 +143,9 @@ class Thread extends CustomElement {
143
143
  */
144
144
  getOption(path, defaultValue = undefined) {
145
145
  if (path === "entries" || path?.startsWith("entries.")) {
146
- try {
147
- return new Pathfinder({
148
- entries: this[entriesSymbol],
149
- }).getVia(path);
150
- } catch (e) {
151
- return defaultValue;
152
- }
146
+ return new Pathfinder({
147
+ entries: this[entriesSymbol],
148
+ }).getVia(path, defaultValue);
153
149
  }
154
150
 
155
151
  return super.getOption(path, defaultValue);
@@ -363,25 +363,6 @@ function getLabels() {
363
363
  }
364
364
  }
365
365
 
366
- /**
367
- * @private
368
- * @param format
369
- * @returns {*[]}
370
- */
371
- function getWeekdays(format = "long") {
372
- const locale = getLocaleOfDocument();
373
-
374
- const weekdays = [];
375
- for (let i = 1; i < 8; i++) {
376
- const date = new Date(1970, 0, 4 + i); // 4. Jan. 1970 = Sonntag
377
- weekdays.push(
378
- new Intl.DateTimeFormat(locale, { weekday: format }).format(date),
379
- );
380
- }
381
-
382
- return weekdays;
383
- }
384
-
385
366
  /**
386
367
  * @private
387
368
  * @return {void}
@@ -363,7 +363,11 @@ function getAppointmentRowsUsingCalendar(
363
363
  * @returns {*[]}
364
364
  */
365
365
  function getWeekdays(format = "long") {
366
- const locale = getLocaleOfDocument();
366
+ // Intl needs the language tag. Handing it the Locale instance makes
367
+ // CanonicalizeLocaleList treat the object as array like, and its missing
368
+ // length yields an empty locale list, so the formatter silently falls back
369
+ // to the host system locale and the document language is ignored.
370
+ const locale = getLocaleOfDocument().toString();
367
371
 
368
372
  const weekdays = [];
369
373
  for (let i = 1; i < 8; i++) {
@@ -181,9 +181,9 @@ function buildFlatMap(subject, selector, key, parentMap) {
181
181
  for (const [k, o] of map) {
182
182
  const copyKey = clone(key);
183
183
 
184
- currentPath.map((a) => {
185
- copyKey.push(a === WILDCARD ? k : a);
186
- });
184
+ for (const segment of currentPath) {
185
+ copyKey.push(segment === WILDCARD ? k : segment);
186
+ }
187
187
 
188
188
  const kk = copyKey.join(DELIMITER);
189
189
  const sub = buildFlatMap.call(
@@ -362,7 +362,8 @@ function buildFlatMap(subject, selector, key, parentMap) {
362
362
  * @return {*}
363
363
  */
364
364
  export function build(subject, definition, defaultValue) {
365
- if (definition === undefined) return defaultValue ? defaultValue : subject;
365
+ if (definition === undefined)
366
+ return defaultValue !== undefined ? defaultValue : subject;
366
367
  validateString(definition);
367
368
 
368
369
  const regexp = /(?<placeholder>\${(?<path>[a-z\^A-Z.\-_0-9]*)})/gm;
@@ -384,7 +385,7 @@ export function build(subject, definition, defaultValue) {
384
385
  let v = finder.getVia(path);
385
386
  if (v === undefined) v = defaultValue;
386
387
 
387
- definition = definition.replaceAll(placeholder, v);
388
+ definition = definition.replaceAll(placeholder, () => `${v}`);
388
389
  });
389
390
 
390
391
  return definition;
@@ -21,12 +21,6 @@ import { extend } from "./extend.mjs";
21
21
 
22
22
  export { buildTree };
23
23
 
24
- /**
25
- * @private
26
- * @type {symbol}
27
- */
28
- const parentSymbol = Symbol("parent");
29
-
30
24
  /**
31
25
  * @private
32
26
  * @type {symbol}
@@ -171,8 +165,6 @@ function buildTree(subject, selector, idKey, parentIDKey, options) {
171
165
  throw new Error("the object has no value for the specified id");
172
166
  }
173
167
 
174
- o[parentSymbol] = ref;
175
-
176
168
  const node = new Node(o);
177
169
  this.has(ref)
178
170
  ? this.get(ref).add(node)
@@ -165,16 +165,9 @@ class Datasource extends Base {
165
165
  * @return {*}
166
166
  */
167
167
  getOption(path, defaultValue) {
168
- let value;
169
-
170
- try {
171
- value = new Pathfinder(
172
- this[internalSymbol].getRealSubject()["options"],
173
- ).getVia(path);
174
- } catch (e) {}
175
-
176
- if (value === undefined) return defaultValue;
177
- return value;
168
+ return new Pathfinder(
169
+ this[internalSymbol].getRealSubject()["options"],
170
+ ).getVia(path, defaultValue);
178
171
  }
179
172
 
180
173
  /**
@@ -35,20 +35,15 @@ function diff(first, second) {
35
35
  }
36
36
 
37
37
  /**
38
+ * Own keys of both sides. Arrays are covered by this as well: their indices are
39
+ * own enumerable keys, and reporting them as string path segments is the
40
+ * established contract for every diff path.
38
41
  * @private
39
42
  * @param a
40
43
  * @param b
41
- * @param type
42
- * @return {Set<string>|Set<number>}
44
+ * @return {Set<string>}
43
45
  */
44
- function getKeys(a, b, type) {
45
- if (isArray(type)) {
46
- const keys =
47
- a.length > b.length ? new Array(a.length) : new Array(b.length);
48
- keys.fill(0);
49
- return new Set(keys.map((_, i) => i));
50
- }
51
-
46
+ function getKeys(a, b) {
52
47
  return new Set(Object.keys(a).concat(Object.keys(b)));
53
48
  }
54
49
 
@@ -68,7 +63,7 @@ function doDiff(a, b, path, diff) {
68
63
  const currDiff = diff || [];
69
64
 
70
65
  if (typeA === typeB && (typeA === "object" || typeA === "array")) {
71
- const keys = getKeys(a, b, typeA);
66
+ const keys = getKeys(a, b);
72
67
  if (keys.size === 0) {
73
68
  return currDiff;
74
69
  }
@@ -83,7 +78,7 @@ function doDiff(a, b, path, diff) {
83
78
  }
84
79
  });
85
80
  } else {
86
- const o = getOperator(a, b, typeA, typeB);
81
+ const o = getOperator(a, b);
87
82
  if (o !== undefined) {
88
83
  currDiff.push(buildResult(a, b, o, path));
89
84
  }
@@ -153,6 +148,12 @@ function isNotEqual(a, b) {
153
148
  return a.getTime() !== b.getTime();
154
149
  }
155
150
 
151
+ // Two NaN values at the same path are the same value. Object.is() is not used
152
+ // here because it would additionally report 0 and -0 as a change.
153
+ if (Number.isNaN(a) && Number.isNaN(b)) {
154
+ return false;
155
+ }
156
+
156
157
  return a !== b;
157
158
  }
158
159
 
@@ -33,6 +33,16 @@ const unsafeMutationKeys = new Set(["__proto__", "prototype", "constructor"]);
33
33
  * @param args
34
34
  */
35
35
  function extend(...args) {
36
+ return extendValues(args, new Map());
37
+ }
38
+
39
+ /**
40
+ * @private
41
+ * @param {array} args
42
+ * @param {Map} path source objects currently being merged, mapped to their target
43
+ * @return {object}
44
+ */
45
+ function extendValues(args, path) {
36
46
  let o;
37
47
  let i;
38
48
 
@@ -65,6 +75,13 @@ function extend(...args) {
65
75
  }
66
76
 
67
77
  if ((isObject(v) && typeOf(v) === "object") || isArray(v)) {
78
+ // A source object that is still being merged further up the call
79
+ // chain is a cycle. Reuse its target instead of recursing forever.
80
+ if (path.has(v)) {
81
+ o[k] = path.get(v);
82
+ continue;
83
+ }
84
+
68
85
  if (o[k] === undefined) {
69
86
  if (isArray(v)) {
70
87
  o[k] = [];
@@ -87,7 +104,12 @@ function extend(...args) {
87
104
  continue;
88
105
  }
89
106
 
90
- o[k] = extend(o[k], v);
107
+ path.set(v, o[k]);
108
+ try {
109
+ o[k] = extendValues([o[k], v], path);
110
+ } finally {
111
+ path.delete(v);
112
+ }
91
113
  } else {
92
114
  if (isArray(o)) {
93
115
  o.push(v);
@@ -133,8 +133,14 @@ class Pathfinder extends Base {
133
133
  }
134
134
 
135
135
  /**
136
+ * Reads the value at the given path.
137
+ *
138
+ * Without a second argument a path that cannot be traversed throws, and a
139
+ * missing leaf resolves to undefined. With a second argument the value is
140
+ * returned instead of the error, and also instead of an undefined result.
136
141
  *
137
142
  * @param {string|array} path
143
+ * @param {*} defaultValue returned when the path cannot be resolved
138
144
  * @since 1.4.0
139
145
  * @return {*}
140
146
  * @throws {TypeError} unsupported type
@@ -143,8 +149,18 @@ class Pathfinder extends Base {
143
149
  * @throws {TypeError} value is not an integer
144
150
  * @throws {Error} unsupported action for this data type
145
151
  */
146
- getVia(path) {
147
- return getValueViaPath.call(this, this.object, path);
152
+ getVia(path, defaultValue) {
153
+ // Without a second argument the throwing contract is unchanged.
154
+ if (arguments.length < 2) {
155
+ return getValueViaPath.call(this, this.object, path);
156
+ }
157
+
158
+ const value = resolveValueViaPath.call(this, this.object, path, false);
159
+ if (value === NOT_FOUND || value === undefined) {
160
+ return defaultValue;
161
+ }
162
+
163
+ return value;
148
164
  }
149
165
 
150
166
  /**
@@ -189,13 +205,173 @@ class Pathfinder extends Base {
189
205
  * @since 1.4.0
190
206
  */
191
207
  exists(path) {
208
+ return (
209
+ resolveValueViaPath.call(this, this.object, path, true) !== NOT_FOUND
210
+ );
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Returned instead of throwing when a path cannot be resolved.
216
+ * @private
217
+ * @type {symbol}
218
+ */
219
+ const NOT_FOUND = Symbol("@schukai/monster/data/pathfinder@@notFound");
220
+
221
+ /**
222
+ * Split paths are memoized. getOption() resolves about 1393 option paths across
223
+ * the library, and the same handful of strings is used over and over.
224
+ * @private
225
+ * @type {Map<string, string[]>}
226
+ */
227
+ const pathSplitCache = new Map();
228
+
229
+ /**
230
+ * @private
231
+ * @type {number}
232
+ */
233
+ const PATH_SPLIT_CACHE_LIMIT = 2048;
234
+
235
+ /**
236
+ * The returned array is shared and must never be mutated by the caller.
237
+ * @private
238
+ * @param {string} path
239
+ * @return {string[]}
240
+ */
241
+ function splitPath(path) {
242
+ const cached = pathSplitCache.get(path);
243
+ if (cached !== undefined) {
244
+ return cached;
245
+ }
246
+
247
+ const parts = path.split(DELIMITER);
248
+
249
+ if (pathSplitCache.size >= PATH_SPLIT_CACHE_LIMIT) {
250
+ pathSplitCache.clear();
251
+ }
252
+ pathSplitCache.set(path, parts);
253
+
254
+ return parts;
255
+ }
256
+
257
+ /**
258
+ * A complete integer path segment, or undefined when the segment is not one.
259
+ * @private
260
+ * @param {*} value
261
+ * @return {number|undefined}
262
+ */
263
+ function toPathIndex(value) {
264
+ if (!/^-?\d+$/.test(`${value}`)) {
265
+ return undefined;
266
+ }
267
+
268
+ const index = Number(value);
269
+ if (!Number.isInteger(index)) {
270
+ return undefined;
271
+ }
272
+
273
+ return index;
274
+ }
275
+
276
+ /**
277
+ * Non throwing counterpart of getValueViaPath(). Returns NOT_FOUND instead of
278
+ * throwing, so that resolving an option does not construct an exception, and
279
+ * walks the path iteratively instead of copying and shifting it per segment.
280
+ *
281
+ * The membership semantics of `check` are the ones exists() relies on: a key
282
+ * that is present with value undefined counts as found, and a malformed path is
283
+ * simply not found rather than an error.
284
+ *
285
+ * @private
286
+ * @param {*} subject
287
+ * @param {string|array} path
288
+ * @param {boolean} check
289
+ * @return {*|symbol} the resolved value or NOT_FOUND
290
+ */
291
+ function resolveValueViaPath(subject, path, check) {
292
+ let parts;
293
+ if (isArray(path)) {
294
+ if (path.length === 0) {
295
+ return subject;
296
+ }
297
+ parts = path;
298
+ } else if (isString(path)) {
299
+ if (path === "") {
300
+ return subject;
301
+ }
302
+ parts = splitPath(path);
303
+ } else {
304
+ return NOT_FOUND;
305
+ }
306
+
307
+ // A wildcard builds a Map through iterate(), which stays on the throwing
308
+ // implementation. Those paths are rare, so fall back for them.
309
+ if (parts.includes(this.wildCard)) {
192
310
  try {
193
- getValueViaPath.call(this, this.object, path, true);
194
- return true;
195
- } catch (e) {}
311
+ return getValueViaPath.call(this, subject, path, check === true);
312
+ } catch (e) {
313
+ return NOT_FOUND;
314
+ }
315
+ }
316
+
317
+ let cursor = subject;
318
+ const last = parts.length - 1;
319
+
320
+ for (let i = 0; i <= last; i++) {
321
+ if (!(isObject(cursor) || isArray(cursor))) {
322
+ return NOT_FOUND;
323
+ }
324
+
325
+ if (cursor instanceof WeakSet) {
326
+ return NOT_FOUND;
327
+ }
328
+
329
+ if (typeof WeakRef === "function" && cursor instanceof WeakRef) {
330
+ return NOT_FOUND;
331
+ }
332
+
333
+ const segment = parts[i];
334
+ const isLast = i === last;
335
+ let value;
336
+
337
+ if (cursor instanceof Map || cursor instanceof WeakMap) {
338
+ if (isLast && check === true && !cursor.has(segment)) {
339
+ return NOT_FOUND;
340
+ }
341
+ value = cursor.get(segment);
342
+ } else if (cursor instanceof Set) {
343
+ const index = toPathIndex(segment);
344
+ if (index === undefined) {
345
+ return NOT_FOUND;
346
+ }
347
+ if (isLast && check === true && !(index >= 0 && index < cursor.size)) {
348
+ return NOT_FOUND;
349
+ }
350
+ value = [...cursor][index];
351
+ } else if (isArray(cursor)) {
352
+ const index = toPathIndex(segment);
353
+ if (index === undefined) {
354
+ return NOT_FOUND;
355
+ }
356
+ if (isLast && check === true && !Reflect.has(cursor, index)) {
357
+ return NOT_FOUND;
358
+ }
359
+ value = cursor[index];
360
+ } else {
361
+ if (isLast && check === true && !Reflect.has(cursor, segment)) {
362
+ return NOT_FOUND;
363
+ }
364
+ value = cursor[segment];
365
+ }
366
+
367
+ if (isLast) {
368
+ return value;
369
+ }
196
370
 
197
- return false;
371
+ cursor = value;
198
372
  }
373
+
374
+ return NOT_FOUND;
199
375
  }
200
376
 
201
377
  /**
@@ -217,16 +393,21 @@ function iterate(subject, path, check) {
217
393
 
218
394
  const result = new Map();
219
395
 
220
- if (isArray(path)) {
221
- path = path.join(DELIMITER);
222
- }
223
-
224
- if (isObject(subject) || isArray(subject)) {
396
+ if (subject instanceof Map) {
397
+ for (const [key, value] of subject) {
398
+ result.set(key, getValueViaPath.call(this, value, path, check));
399
+ }
400
+ } else if (subject instanceof Set) {
401
+ let index = 0;
402
+ for (const value of subject) {
403
+ result.set(`${index++}`, getValueViaPath.call(this, value, path, check));
404
+ }
405
+ } else if (isObject(subject) || isArray(subject)) {
225
406
  for (const [key, value] of Object.entries(subject)) {
226
407
  result.set(key, getValueViaPath.call(this, value, path, check));
227
408
  }
228
409
  } else {
229
- const key = path.split(DELIMITER).shift();
410
+ const key = isArray(path) ? path[0] : path.split(DELIMITER).shift();
230
411
  result.set(key, getValueViaPath.call(this, subject, path, check));
231
412
  }
232
413
 
@@ -272,7 +453,7 @@ function getValueViaPath(subject, path, check) {
272
453
  let current = parts.shift();
273
454
 
274
455
  if (current === this.wildCard) {
275
- return iterate.call(this, subject, parts.join(DELIMITER), check);
456
+ return iterate.call(this, subject, parts, check);
276
457
  }
277
458
 
278
459
  if (isObject(subject) || isArray(subject)) {
@@ -280,23 +461,21 @@ function getValueViaPath(subject, path, check) {
280
461
  if (subject instanceof Map || subject instanceof WeakMap) {
281
462
  anchor = subject.get(current);
282
463
  } else if (subject instanceof Set) {
283
- current = parseInt(current);
284
- validateInteger(current);
464
+ current = parsePathIndex(current);
285
465
  anchor = [...subject]?.[current];
286
466
  } else if (subject instanceof WeakSet) {
287
467
  throw Error("unsupported action for this data type (WeakSet)");
288
468
  } else if (typeof WeakRef === "function" && subject instanceof WeakRef) {
289
469
  throw Error("unsupported action for this data type (WeakRef)");
290
470
  } else if (isArray(subject)) {
291
- current = parseInt(current);
292
- validateInteger(current);
471
+ current = parsePathIndex(current);
293
472
  anchor = subject?.[current];
294
473
  } else {
295
474
  anchor = subject?.[current];
296
475
  }
297
476
 
298
477
  if (isObject(anchor) || isArray(anchor)) {
299
- return getValueViaPath.call(this, anchor, parts.join(DELIMITER), check);
478
+ return getValueViaPath.call(this, anchor, parts, check);
300
479
  }
301
480
 
302
481
  if (parts.length > 0) {
@@ -304,12 +483,18 @@ function getValueViaPath(subject, path, check) {
304
483
  }
305
484
 
306
485
  if (check === true) {
307
- const descriptor = Object.getOwnPropertyDescriptor(
308
- Object.getPrototypeOf(subject),
309
- current,
310
- );
486
+ let exists;
487
+ if (subject instanceof Map || subject instanceof WeakMap) {
488
+ exists = subject.has(current);
489
+ } else if (subject instanceof Set) {
490
+ exists = current >= 0 && current < subject.size;
491
+ } else if (isArray(subject)) {
492
+ exists = Reflect.has(subject, current);
493
+ } else {
494
+ exists = Reflect.has(subject, current);
495
+ }
311
496
 
312
- if (!subject.hasOwnProperty(current) && descriptor === undefined) {
497
+ if (!exists) {
313
498
  throw Error("unknown value " + current);
314
499
  }
315
500
  }
@@ -411,16 +596,25 @@ function setValueViaPath(subject, path, value) {
411
596
  * @param {*} value
412
597
  */
413
598
  function assignProperty(object, key, value) {
414
- if (!object.hasOwnProperty(key)) {
415
- object[key] = value;
416
- return;
417
- }
599
+ // Writing undefined keeps the key. Deletion is deliberately left to
600
+ // deleteVia(), because a default option declared as undefined must stay
601
+ // present for setOptionFromAttribute() to keep accepting changes for it.
602
+ object[key] = value;
603
+ }
418
604
 
419
- if (value === undefined) {
420
- delete object[key];
605
+ /**
606
+ * Convert a complete integer path segment without accepting trailing text.
607
+ * @param {*} value
608
+ * @return {number}
609
+ * @private
610
+ */
611
+ function parsePathIndex(value) {
612
+ if (!/^-?\d+$/.test(`${value}`)) {
613
+ throw new TypeError("value is not an integer");
421
614
  }
422
-
423
- object[key] = value;
615
+ const index = Number(value);
616
+ validateInteger(index);
617
+ return index;
424
618
  }
425
619
 
426
620
  /**
@@ -486,7 +680,9 @@ function deleteValueViaPath(subject, path) {
486
680
  * @private
487
681
  */
488
682
  function validateMutationPath(path) {
489
- const parts = isArray(path) ? path : path.split(DELIMITER);
683
+ const parts = (isArray(path) ? path : [path]).flatMap((part) =>
684
+ `${part}`.split(DELIMITER),
685
+ );
490
686
  for (const part of parts) {
491
687
  if (UNSAFE_MUTATION_PATH_SEGMENTS.has(`${part}`)) {
492
688
  throw new Error(`unsafe mutation path segment: ${part}`);
@@ -530,7 +530,7 @@ function transform(value) {
530
530
 
531
531
  const useKey = keys?.[key];
532
532
 
533
- if (value?.[useKey]) {
533
+ if (useKey !== undefined && Reflect.has(value, useKey)) {
534
534
  return value?.[useKey];
535
535
  }
536
536
 
@@ -555,7 +555,7 @@ function transform(value) {
555
555
  }
556
556
 
557
557
  if (isObject(value) || isArray(value)) {
558
- if (value?.[key]) {
558
+ if (Reflect.has(value, key)) {
559
559
  return value?.[key];
560
560
  }
561
561