@rohal12/spindle 0.52.2 → 0.52.3

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.
@@ -1,14 +1,17 @@
1
1
  /**
2
- * Variable namespaces: the records holding $story variables, _temporary
3
- * variables, %transient variables and @locals, keyed by variable name.
2
+ * Name-keyed records: the variable namespaces ($story variables, _temporary
3
+ * variables, %transient variables and @locals), the passage counters and
4
+ * the other records keyed by names the author chooses.
4
5
  *
5
6
  * They have no prototype, so every name is plain storage: a variable may be
6
7
  * called `constructor`, `toString` or `hasOwnProperty`, and reading one
7
8
  * that was never set gives undefined rather than an Object.prototype
8
- * member. The one name they cannot hold is `__proto__`: story state (and so
9
- * saves) never holds a property by that name, and on an ordinary object
10
- * writing it would replace the prototype instead of storing a value. It is
11
- * refused wherever a variable name enters (checkVariableName).
9
+ * member. Records that may have a prototype (a loaded save, a caller's
10
+ * object) are read through ownValue(), which only sees their own entries.
11
+ * The one variable name a namespace cannot hold is `__proto__`: story state
12
+ * (and so saves) never holds a property by that name, and on an ordinary
13
+ * object writing it would replace the prototype instead of storing a value.
14
+ * It is refused wherever a variable name enters (checkVariableName).
12
15
  */
13
16
 
14
17
  export type Namespace = Record<string, unknown>;
@@ -17,31 +20,58 @@ export type Namespace = Record<string, unknown>;
17
20
  export const RESERVED_NAME = '__proto__';
18
21
 
19
22
  /**
20
- * Throw a TypeError if `name` (without its sigil) cannot be a variable
21
- * name. `label` is the name as the author wrote it (e.g. `$__proto__`).
23
+ * Why `name` (without its sigil) cannot be a variable name, or undefined if
24
+ * it can. `label` is the name as the author wrote it (e.g. `$__proto__`).
22
25
  */
26
+ export function variableNameError(
27
+ name: string,
28
+ label = name,
29
+ ): string | undefined {
30
+ return name === RESERVED_NAME
31
+ ? `"${label}" cannot be used as a variable name (${RESERVED_NAME} is reserved)`
32
+ : undefined;
33
+ }
34
+
35
+ /** Throw a TypeError if `name` cannot be a variable name. */
23
36
  export function checkVariableName(name: string, label = name): void {
24
- if (name === RESERVED_NAME) {
25
- throw new TypeError(
26
- `spindle: "${label}" cannot be used as a variable name (${RESERVED_NAME} is reserved)`,
27
- );
28
- }
37
+ const error = variableNameError(name, label);
38
+ if (error) throw new TypeError(`spindle: ${error}`);
29
39
  }
30
40
 
31
- /** A namespace with no prototype holding the own entries of `sources`. */
32
- export function createNamespace(
33
- ...sources: readonly (object | null | undefined)[]
34
- ): Namespace {
35
- const ns = Object.create(null) as Namespace;
41
+ /** Whether `obj` holds `key` as an own property. */
42
+ export const hasOwn = (obj: object, key: PropertyKey): boolean =>
43
+ Object.prototype.hasOwnProperty.call(obj, key);
44
+
45
+ /** `ns[key]` if `ns` holds it as an own property, else undefined. */
46
+ export const ownValue = (
47
+ ns: object | null | undefined,
48
+ key: string,
49
+ ): unknown => (ns && hasOwn(ns, key) ? (ns as Namespace)[key] : undefined);
50
+
51
+ /**
52
+ * A record with no prototype holding the own entries of `sources`, or only
53
+ * those whose value passes `keep`.
54
+ */
55
+ function ownEntries<T>(
56
+ sources: readonly (object | null | undefined)[],
57
+ keep: (value: unknown) => boolean = () => true,
58
+ ): Record<string, T> {
59
+ const record = Object.create(null) as Record<string, T>;
36
60
  for (const source of sources) {
37
61
  if (!source) continue;
38
62
  for (const key of Object.keys(source)) {
39
- ns[key] = (source as Namespace)[key];
63
+ const value = (source as Namespace)[key];
64
+ if (keep(value)) record[key] = value as T;
40
65
  }
41
66
  }
42
- return ns;
67
+ return record;
43
68
  }
44
69
 
70
+ /** A namespace with no prototype holding the own entries of `sources`. */
71
+ export const createNamespace = (
72
+ ...sources: readonly (object | null | undefined)[]
73
+ ): Namespace => ownEntries(sources);
74
+
45
75
  /** Whether `value` is an object with no prototype. */
46
76
  export const isNamespace = (value: object): boolean =>
47
77
  Object.getPrototypeOf(value) === null;
@@ -64,8 +94,42 @@ export function withEntry(
64
94
  /** An empty namespace that cannot change. */
65
95
  export const EMPTY_NAMESPACE: Namespace = Object.freeze(createNamespace());
66
96
 
67
- /** `ns[key]` if `ns` holds it as an own property, else undefined. */
68
- export const ownValue = (ns: object, key: string): unknown =>
69
- Object.prototype.hasOwnProperty.call(ns, key)
70
- ? (ns as Namespace)[key]
71
- : undefined;
97
+ /** The names whose own entries differ between `a` and `b`, `a`'s first. */
98
+ export function changedNames(a: object, b: object): string[] {
99
+ const names = new Set([...Object.keys(a), ...Object.keys(b)]);
100
+ return [...names].filter((name) => ownValue(a, name) !== ownValue(b, name));
101
+ }
102
+
103
+ /**
104
+ * Passage counters: the visit and render counts of the story state, keyed
105
+ * by passage name. A passage may have any name, `__proto__` included, so
106
+ * counting one stores an entry instead of replacing the prototype, and a
107
+ * passage that was never counted counts 0. Counters from outside the store
108
+ * (a loaded save) go through createCounts(), which keeps only their own
109
+ * entries that are counts.
110
+ */
111
+ export type Counts = Record<string, number>;
112
+
113
+ const isCount = (value: unknown): value is number =>
114
+ typeof value === 'number' && Number.isInteger(value) && value >= 0;
115
+
116
+ /** The count of `name` in `counts`: its own entry, or 0. */
117
+ export function countOf(
118
+ counts: object | null | undefined,
119
+ name: string,
120
+ ): number {
121
+ const value = ownValue(counts, name);
122
+ return isCount(value) ? value : 0;
123
+ }
124
+
125
+ /**
126
+ * Counters with no prototype holding the own entries of `source` that are
127
+ * counts, with `name` (if given) counted once more. Immer copies the
128
+ * counters on every write anyway, so counting into a fresh copy costs no
129
+ * more than counting in place.
130
+ */
131
+ export function createCounts(source?: object | null, name?: string): Counts {
132
+ const counts = ownEntries<number>([source], isCount);
133
+ if (name !== undefined) counts[name] = countOf(source, name) + 1;
134
+ return counts;
135
+ }
@@ -1,7 +1,5 @@
1
1
  import { isDraft } from 'immer';
2
-
3
- const hasOwn = (obj: object, key: string): boolean =>
4
- Object.prototype.hasOwnProperty.call(obj, key);
2
+ import { hasOwn } from './namespace';
5
3
 
6
4
  /**
7
5
  * Traverse dot-path segments on an object and return the nested value.
@@ -1,45 +0,0 @@
1
- /**
2
- * Passage counters: the visit and render counts of the story state, keyed
3
- * by passage name.
4
- *
5
- * A passage may have any name, including `constructor`, `toString` or
6
- * `__proto__`, so the counters only ever read their own entries: a passage
7
- * that was never counted counts 0, never an Object.prototype member. The
8
- * store keeps them without a prototype, so writing the count of a passage
9
- * named `__proto__` stores an entry instead of replacing the prototype.
10
- * Counters from outside the store (a loaded save) go through
11
- * createCounts(), which keeps only their own entries that are counts.
12
- */
13
-
14
- export type Counts = Record<string, number>;
15
-
16
- const isCount = (value: unknown): value is number =>
17
- typeof value === 'number' && Number.isInteger(value) && value >= 0;
18
-
19
- /** The count of `name` in `counts`: its own entry, or 0. */
20
- export function countOf(
21
- counts: object | null | undefined,
22
- name: string,
23
- ): number {
24
- if (!counts || !Object.prototype.hasOwnProperty.call(counts, name)) return 0;
25
- const value = (counts as Record<string, unknown>)[name];
26
- return isCount(value) ? value : 0;
27
- }
28
-
29
- /**
30
- * Counters with no prototype holding the own entries of `source` that are
31
- * counts, with `name` (if given) counted once more. Immer copies the
32
- * counters on every write anyway, so counting into a fresh copy costs no
33
- * more than counting in place.
34
- */
35
- export function createCounts(source?: object | null, name?: string): Counts {
36
- const counts = Object.create(null) as Counts;
37
- if (source) {
38
- for (const key of Object.keys(source)) {
39
- const value = (source as Record<string, unknown>)[key];
40
- if (isCount(value)) counts[key] = value;
41
- }
42
- }
43
- if (name !== undefined) counts[name] = countOf(source, name) + 1;
44
- return counts;
45
- }