@rohal12/spindle 0.52.1 → 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.
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +163 -162
- package/dist/pkg/story-variables.js +24 -7
- package/package.json +4 -1
- package/src/components/App.tsx +6 -3
- package/src/components/macros/ExprDisplay.tsx +2 -7
- package/src/components/macros/MenubarButtons.tsx +13 -12
- package/src/components/macros/PassageDisplay.tsx +8 -4
- package/src/components/macros/SaveManager.tsx +8 -4
- package/src/components/macros/VarDisplay.tsx +11 -12
- package/src/event-emitter.ts +15 -11
- package/src/expression.ts +44 -42
- package/src/hooks/use-merged-locals.ts +6 -4
- package/src/hooks/use-story-fields.ts +19 -0
- package/src/settings.ts +20 -22
- package/src/store.ts +2 -1
- package/src/story-api.ts +29 -77
- package/src/story-variables.ts +5 -9
- package/src/utils/live-locals.ts +2 -4
- package/src/utils/namespace.ts +89 -25
- package/src/utils/object-path.ts +1 -3
- package/src/utils/counts.ts +0 -45
package/src/utils/namespace.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* variables, %transient variables and @locals,
|
|
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.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
/**
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
63
|
+
const value = (source as Namespace)[key];
|
|
64
|
+
if (keep(value)) record[key] = value as T;
|
|
40
65
|
}
|
|
41
66
|
}
|
|
42
|
-
return
|
|
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
|
-
/**
|
|
68
|
-
export
|
|
69
|
-
Object.
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
}
|
package/src/utils/object-path.ts
CHANGED
package/src/utils/counts.ts
DELETED
|
@@ -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
|
-
}
|