@rohal12/spindle 0.52.2 → 0.52.4
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 +430 -786
- package/dist/pkg/story-variables.js +179 -476
- package/package.json +1 -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/components/macros/Widget.tsx +18 -7
- 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/index.tsx +5 -8
- package/src/interpolation.ts +16 -22
- package/src/js-lexer.ts +20 -9
- package/src/markup/ast.ts +108 -147
- package/src/markup/code-attributes.ts +4 -6
- package/src/markup/tokenizer.ts +218 -574
- 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/settings.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useStoryStore } from './store';
|
|
2
|
-
import { createNamespace, ownValue } from './utils/namespace';
|
|
2
|
+
import { createNamespace, hasOwn, ownValue } from './utils/namespace';
|
|
3
3
|
|
|
4
4
|
export interface ToggleConfig {
|
|
5
5
|
label: string;
|
|
@@ -76,29 +76,30 @@ function loadFromStorage(): void {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/** Register a setting, starting from its default unless it has a value. */
|
|
80
|
+
function define(name: string, def: SettingDef): void {
|
|
81
|
+
definitions.set(name, def);
|
|
82
|
+
if (!hasOwn(values, name)) values[name] = def.config.default;
|
|
83
|
+
loadFromStorage();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The value of setting `name` if it has the type of `fallback`, else that. */
|
|
87
|
+
function valueOf<T>(name: string, fallback: T): T {
|
|
88
|
+
const v = ownValue(values, name);
|
|
89
|
+
return typeof v === typeof fallback ? (v as T) : fallback;
|
|
90
|
+
}
|
|
91
|
+
|
|
79
92
|
export const settings = {
|
|
80
93
|
addToggle(name: string, config: ToggleConfig): void {
|
|
81
|
-
|
|
82
|
-
if (!Object.prototype.hasOwnProperty.call(values, name)) {
|
|
83
|
-
values[name] = config.default;
|
|
84
|
-
}
|
|
85
|
-
loadFromStorage();
|
|
94
|
+
define(name, { type: 'toggle', config });
|
|
86
95
|
},
|
|
87
96
|
|
|
88
97
|
addList(name: string, config: ListConfig): void {
|
|
89
|
-
|
|
90
|
-
if (!Object.prototype.hasOwnProperty.call(values, name)) {
|
|
91
|
-
values[name] = config.default;
|
|
92
|
-
}
|
|
93
|
-
loadFromStorage();
|
|
98
|
+
define(name, { type: 'list', config });
|
|
94
99
|
},
|
|
95
100
|
|
|
96
101
|
addRange(name: string, config: RangeConfig): void {
|
|
97
|
-
|
|
98
|
-
if (!Object.prototype.hasOwnProperty.call(values, name)) {
|
|
99
|
-
values[name] = config.default;
|
|
100
|
-
}
|
|
101
|
-
loadFromStorage();
|
|
102
|
+
define(name, { type: 'range', config });
|
|
102
103
|
},
|
|
103
104
|
|
|
104
105
|
get(name: string): unknown {
|
|
@@ -106,18 +107,15 @@ export const settings = {
|
|
|
106
107
|
},
|
|
107
108
|
|
|
108
109
|
getToggle(name: string): boolean {
|
|
109
|
-
|
|
110
|
-
return typeof v === 'boolean' ? v : false;
|
|
110
|
+
return valueOf(name, false);
|
|
111
111
|
},
|
|
112
112
|
|
|
113
113
|
getList(name: string): string {
|
|
114
|
-
|
|
115
|
-
return typeof v === 'string' ? v : '';
|
|
114
|
+
return valueOf(name, '');
|
|
116
115
|
},
|
|
117
116
|
|
|
118
117
|
getRange(name: string): number {
|
|
119
|
-
|
|
120
|
-
return typeof v === 'number' ? v : 0;
|
|
118
|
+
return valueOf(name, 0);
|
|
121
119
|
},
|
|
122
120
|
|
|
123
121
|
set(name: string, value: unknown): void {
|
package/src/store.ts
CHANGED
|
@@ -62,11 +62,12 @@ import {
|
|
|
62
62
|
} from './execute-mutation';
|
|
63
63
|
import {
|
|
64
64
|
checkVariableName,
|
|
65
|
+
createCounts,
|
|
65
66
|
createNamespace,
|
|
66
67
|
isNamespace,
|
|
68
|
+
type Counts,
|
|
67
69
|
type Namespace,
|
|
68
70
|
} from './utils/namespace';
|
|
69
|
-
import { createCounts, type Counts } from './utils/counts';
|
|
70
71
|
|
|
71
72
|
enablePatches();
|
|
72
73
|
// Story state holds Map and Set values: Immer must be able to draft them
|
package/src/story-api.ts
CHANGED
|
@@ -23,8 +23,8 @@ import { getBackendType } from './saves/storage';
|
|
|
23
23
|
import { registerClass } from './class-registry';
|
|
24
24
|
import { frozenCopy, getActiveMutationScope } from './execute-mutation';
|
|
25
25
|
import { getByPath, setByPath } from './utils/object-path';
|
|
26
|
-
import { checkVariableName } from './utils/namespace';
|
|
27
|
-
import {
|
|
26
|
+
import { changedNames, checkVariableName, ownValue } from './utils/namespace';
|
|
27
|
+
import { countQueries, currentPassage, previousPassage } from './expression';
|
|
28
28
|
import { defineMacro } from './define-macro';
|
|
29
29
|
import type { MacroDefinition } from './define-macro';
|
|
30
30
|
import { getMacroRegistry as _getMacroRegistry } from './registry';
|
|
@@ -83,35 +83,24 @@ function ensureVariableChangedSubscription(): void {
|
|
|
83
83
|
let prevTrans = useStoryStore.getState().transient;
|
|
84
84
|
useStoryStore.subscribe((state) => {
|
|
85
85
|
const changed: Record<string, { from: unknown; to: unknown }> = {};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
hasChanges = true;
|
|
86
|
+
const collect = (
|
|
87
|
+
prev: Record<string, unknown>,
|
|
88
|
+
next: Record<string, unknown>,
|
|
89
|
+
prefix: string,
|
|
90
|
+
) => {
|
|
91
|
+
for (const key of changedNames(prev, next)) {
|
|
92
|
+
changed[prefix + key] = {
|
|
93
|
+
from: ownValue(prev, key),
|
|
94
|
+
to: ownValue(next, key),
|
|
95
|
+
};
|
|
97
96
|
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const allTransKeys = new Set([
|
|
102
|
-
...Object.keys(prevTrans),
|
|
103
|
-
...Object.keys(state.transient),
|
|
104
|
-
]);
|
|
105
|
-
for (const key of allTransKeys) {
|
|
106
|
-
if (state.transient[key] !== prevTrans[key]) {
|
|
107
|
-
changed[`%${key}`] = { from: prevTrans[key], to: state.transient[key] };
|
|
108
|
-
hasChanges = true;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
97
|
+
};
|
|
98
|
+
collect(prevVars, state.variables, '');
|
|
99
|
+
collect(prevTrans, state.transient, '%');
|
|
111
100
|
|
|
112
101
|
prevVars = state.variables;
|
|
113
102
|
prevTrans = state.transient;
|
|
114
|
-
if (
|
|
103
|
+
if (Object.keys(changed).length > 0) {
|
|
115
104
|
emit('variableChanged', changed);
|
|
116
105
|
}
|
|
117
106
|
});
|
|
@@ -271,6 +260,8 @@ function setOne(draft: VariableNamespaces, name: string, value: unknown): void {
|
|
|
271
260
|
}
|
|
272
261
|
|
|
273
262
|
function createStoryAPI(): StoryAPI {
|
|
263
|
+
const visits = countQueries(() => useStoryStore.getState().visitCounts);
|
|
264
|
+
const renders = countQueries(() => useStoryStore.getState().renderCounts);
|
|
274
265
|
return {
|
|
275
266
|
get(name: string): unknown {
|
|
276
267
|
const { isTransient, key } = parseName(name);
|
|
@@ -284,7 +275,7 @@ function createStoryAPI(): StoryAPI {
|
|
|
284
275
|
const namespace = isTransient ? source.transient : source.variables;
|
|
285
276
|
const value = key.includes('.')
|
|
286
277
|
? getByPath(namespace, key.split('.'))
|
|
287
|
-
: namespace
|
|
278
|
+
: ownValue(namespace, key);
|
|
288
279
|
return scope ? frozenCopy(value) : value;
|
|
289
280
|
},
|
|
290
281
|
|
|
@@ -357,55 +348,16 @@ function createStoryAPI(): StoryAPI {
|
|
|
357
348
|
return useStoryStore.getState().importSave(data, slot);
|
|
358
349
|
},
|
|
359
350
|
|
|
360
|
-
visited
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const { visitCounts } = useStoryStore.getState();
|
|
371
|
-
return names.some((n) => countOf(visitCounts, n) > 0);
|
|
372
|
-
},
|
|
373
|
-
|
|
374
|
-
hasVisitedAll(...names: string[]): boolean {
|
|
375
|
-
const { visitCounts } = useStoryStore.getState();
|
|
376
|
-
return names.every((n) => countOf(visitCounts, n) > 0);
|
|
377
|
-
},
|
|
378
|
-
|
|
379
|
-
rendered(name?: string): number {
|
|
380
|
-
const state = useStoryStore.getState();
|
|
381
|
-
return countOf(state.renderCounts, name ?? state.currentPassage);
|
|
382
|
-
},
|
|
383
|
-
|
|
384
|
-
hasRendered(name?: string): boolean {
|
|
385
|
-
return this.rendered(name) > 0;
|
|
386
|
-
},
|
|
387
|
-
|
|
388
|
-
hasRenderedAny(...names: string[]): boolean {
|
|
389
|
-
const { renderCounts } = useStoryStore.getState();
|
|
390
|
-
return names.some((n) => countOf(renderCounts, n) > 0);
|
|
391
|
-
},
|
|
392
|
-
|
|
393
|
-
hasRenderedAll(...names: string[]): boolean {
|
|
394
|
-
const { renderCounts } = useStoryStore.getState();
|
|
395
|
-
return names.every((n) => countOf(renderCounts, n) > 0);
|
|
396
|
-
},
|
|
397
|
-
|
|
398
|
-
currentPassage(): Passage | undefined {
|
|
399
|
-
const state = useStoryStore.getState();
|
|
400
|
-
return state.storyData?.passages.get(state.currentPassage);
|
|
401
|
-
},
|
|
402
|
-
|
|
403
|
-
previousPassage(): Passage | undefined {
|
|
404
|
-
const state = useStoryStore.getState();
|
|
405
|
-
if (state.historyIndex <= 0) return undefined;
|
|
406
|
-
const prevName = state.history[state.historyIndex - 1]?.passage;
|
|
407
|
-
return prevName ? state.storyData?.passages.get(prevName) : undefined;
|
|
408
|
-
},
|
|
351
|
+
visited: visits.count,
|
|
352
|
+
hasVisited: visits.has,
|
|
353
|
+
hasVisitedAny: visits.any,
|
|
354
|
+
hasVisitedAll: visits.all,
|
|
355
|
+
rendered: renders.count,
|
|
356
|
+
hasRendered: renders.has,
|
|
357
|
+
hasRenderedAny: renders.any,
|
|
358
|
+
hasRenderedAll: renders.all,
|
|
359
|
+
currentPassage,
|
|
360
|
+
previousPassage,
|
|
409
361
|
|
|
410
362
|
get title(): string {
|
|
411
363
|
return useStoryStore.getState().storyData?.name || '';
|
package/src/story-variables.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { tokenize, type Token } from './markup/tokenizer';
|
|
|
3
3
|
import { isCodeAttribute, splitSigilTemplate } from './markup/code-attributes';
|
|
4
4
|
import { errorMessage } from './utils/error-message';
|
|
5
5
|
import { lexJs, scanStringLiteral, type JsGoal } from './js-lexer';
|
|
6
|
-
import { createNamespace,
|
|
6
|
+
import { createNamespace, variableNameError } from './utils/namespace';
|
|
7
7
|
|
|
8
8
|
export type VarType = 'number' | 'string' | 'boolean' | 'array' | 'object';
|
|
9
9
|
|
|
@@ -80,11 +80,8 @@ export function parseStoryVariables(
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
const [, name, expr] = match as [string, string, string];
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
`${passageName}: "${sigil}${name}" cannot be used as a variable name (${RESERVED_NAME} is reserved)`,
|
|
86
|
-
);
|
|
87
|
-
}
|
|
83
|
+
const nameError = variableNameError(name, sigil + name);
|
|
84
|
+
if (nameError) throw new Error(`${passageName}: ${nameError}`);
|
|
88
85
|
let value: unknown;
|
|
89
86
|
try {
|
|
90
87
|
value = new Function('return (' + expr + ')')();
|
|
@@ -134,9 +131,8 @@ function validateRef(
|
|
|
134
131
|
const parts = ref.split('.');
|
|
135
132
|
const rootName = parts[0]!;
|
|
136
133
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
134
|
+
const nameError = variableNameError(rootName, '$' + rootName);
|
|
135
|
+
if (nameError) return nameError;
|
|
140
136
|
|
|
141
137
|
// Skip for-loop locals
|
|
142
138
|
if (forLocals.has(rootName)) return null;
|
package/src/utils/live-locals.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createNamespace, ownValue } from './namespace';
|
|
1
|
+
import { createNamespace, hasOwn, ownValue } from './namespace';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Read-only view of a locals scope that always reflects its current values.
|
|
@@ -16,9 +16,7 @@ export function liveLocalsView(
|
|
|
16
16
|
return new Proxy(createNamespace(), {
|
|
17
17
|
get: (_, key) =>
|
|
18
18
|
typeof key === 'string' ? ownValue(getValues(), key) : undefined,
|
|
19
|
-
has: (_, key) =>
|
|
20
|
-
typeof key === 'string' &&
|
|
21
|
-
Object.prototype.hasOwnProperty.call(getValues(), key),
|
|
19
|
+
has: (_, key) => typeof key === 'string' && hasOwn(getValues(), key),
|
|
22
20
|
ownKeys: () => Reflect.ownKeys(getValues()),
|
|
23
21
|
getOwnPropertyDescriptor: (_, key) => {
|
|
24
22
|
const desc = Object.getOwnPropertyDescriptor(getValues(), key);
|
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
|
-
}
|