@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.
- 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 +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/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/dist/pkg/headless.js
CHANGED
|
@@ -4,6 +4,7 @@ import { useSyncExternalStore } from "preact/compat";
|
|
|
4
4
|
import { createStore } from "zustand/vanilla";
|
|
5
5
|
import { immer } from "zustand/middleware/immer";
|
|
6
6
|
import { Immer, applyPatches, current, enableMapSet, enablePatches, freeze, isDraft, produceWithPatches } from "immer";
|
|
7
|
+
import { shallow } from "zustand/vanilla/shallow";
|
|
7
8
|
import { Fragment as Fragment$1, jsx, jsxs } from "preact/jsx-runtime";
|
|
8
9
|
import { micromark } from "micromark";
|
|
9
10
|
import { gfmTable, gfmTableHtml } from "micromark-extension-gfm-table";
|
|
@@ -2804,15 +2805,28 @@ function buildAST(tokens) {
|
|
|
2804
2805
|
return root;
|
|
2805
2806
|
}
|
|
2806
2807
|
//#endregion
|
|
2808
|
+
//#region src/hooks/use-story-fields.ts
|
|
2809
|
+
/**
|
|
2810
|
+
* Story state fields `keys`, re-rendering when any of them changes (as one
|
|
2811
|
+
* useStoryStore() selector per field would). The result keeps its identity
|
|
2812
|
+
* while the fields don't change, as the store's snapshot must.
|
|
2813
|
+
*/
|
|
2814
|
+
function useStoryFields(...keys) {
|
|
2815
|
+
const prev = useRef();
|
|
2816
|
+
return useStoryStore((s) => {
|
|
2817
|
+
const next = {};
|
|
2818
|
+
for (const key of keys) next[key] = s[key];
|
|
2819
|
+
return shallow(prev.current, next) ? prev.current : prev.current = next;
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2822
|
+
//#endregion
|
|
2807
2823
|
//#region src/hooks/use-merged-locals.ts
|
|
2808
2824
|
/**
|
|
2809
2825
|
* Return store variables, temporary, locals from context, and transient.
|
|
2810
2826
|
* All four dicts use unprefixed keys suitable for evaluate/execute.
|
|
2811
2827
|
*/
|
|
2812
2828
|
function useMergedLocals() {
|
|
2813
|
-
const variables =
|
|
2814
|
-
const temporary = useStoryStore((s) => s.temporary);
|
|
2815
|
-
const transient = useStoryStore((s) => s.transient);
|
|
2829
|
+
const { variables, temporary, transient } = useStoryFields("variables", "temporary", "transient");
|
|
2816
2830
|
const localsValues = useContext(LocalsValuesContext);
|
|
2817
2831
|
return useMemo(() => {
|
|
2818
2832
|
return [
|
|
@@ -2957,21 +2971,38 @@ function getPRNGPull() {
|
|
|
2957
2971
|
/** The variable name no namespace can hold. */
|
|
2958
2972
|
var RESERVED_NAME = "__proto__";
|
|
2959
2973
|
/**
|
|
2960
|
-
*
|
|
2961
|
-
*
|
|
2974
|
+
* Why `name` (without its sigil) cannot be a variable name, or undefined if
|
|
2975
|
+
* it can. `label` is the name as the author wrote it (e.g. `$__proto__`).
|
|
2962
2976
|
*/
|
|
2977
|
+
function variableNameError(name, label = name) {
|
|
2978
|
+
return name === "__proto__" ? `"${label}" cannot be used as a variable name (${RESERVED_NAME} is reserved)` : void 0;
|
|
2979
|
+
}
|
|
2980
|
+
/** Throw a TypeError if `name` cannot be a variable name. */
|
|
2963
2981
|
function checkVariableName(name, label = name) {
|
|
2964
|
-
|
|
2982
|
+
const error = variableNameError(name, label);
|
|
2983
|
+
if (error) throw new TypeError(`spindle: ${error}`);
|
|
2965
2984
|
}
|
|
2966
|
-
/**
|
|
2967
|
-
|
|
2968
|
-
|
|
2985
|
+
/** Whether `obj` holds `key` as an own property. */
|
|
2986
|
+
var hasOwn$2 = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
2987
|
+
/** `ns[key]` if `ns` holds it as an own property, else undefined. */
|
|
2988
|
+
var ownValue = (ns, key) => ns && hasOwn$2(ns, key) ? ns[key] : void 0;
|
|
2989
|
+
/**
|
|
2990
|
+
* A record with no prototype holding the own entries of `sources`, or only
|
|
2991
|
+
* those whose value passes `keep`.
|
|
2992
|
+
*/
|
|
2993
|
+
function ownEntries(sources, keep = () => true) {
|
|
2994
|
+
const record = Object.create(null);
|
|
2969
2995
|
for (const source of sources) {
|
|
2970
2996
|
if (!source) continue;
|
|
2971
|
-
for (const key of Object.keys(source))
|
|
2997
|
+
for (const key of Object.keys(source)) {
|
|
2998
|
+
const value = source[key];
|
|
2999
|
+
if (keep(value)) record[key] = value;
|
|
3000
|
+
}
|
|
2972
3001
|
}
|
|
2973
|
-
return
|
|
3002
|
+
return record;
|
|
2974
3003
|
}
|
|
3004
|
+
/** A namespace with no prototype holding the own entries of `sources`. */
|
|
3005
|
+
var createNamespace = (...sources) => ownEntries(sources);
|
|
2975
3006
|
/** Whether `value` is an object with no prototype. */
|
|
2976
3007
|
var isNamespace = (value) => Object.getPrototypeOf(value) === null;
|
|
2977
3008
|
/** `ns` itself if it has no prototype, else a namespace copy of it. */
|
|
@@ -2984,15 +3015,14 @@ function withEntry(ns, key, value) {
|
|
|
2984
3015
|
}
|
|
2985
3016
|
/** An empty namespace that cannot change. */
|
|
2986
3017
|
var EMPTY_NAMESPACE = Object.freeze(createNamespace());
|
|
2987
|
-
/**
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
3018
|
+
/** The names whose own entries differ between `a` and `b`, `a`'s first. */
|
|
3019
|
+
function changedNames(a, b) {
|
|
3020
|
+
return [...new Set([...Object.keys(a), ...Object.keys(b)])].filter((name) => ownValue(a, name) !== ownValue(b, name));
|
|
3021
|
+
}
|
|
2991
3022
|
var isCount = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
2992
3023
|
/** The count of `name` in `counts`: its own entry, or 0. */
|
|
2993
3024
|
function countOf(counts, name) {
|
|
2994
|
-
|
|
2995
|
-
const value = counts[name];
|
|
3025
|
+
const value = ownValue(counts, name);
|
|
2996
3026
|
return isCount(value) ? value : 0;
|
|
2997
3027
|
}
|
|
2998
3028
|
/**
|
|
@@ -3002,11 +3032,7 @@ function countOf(counts, name) {
|
|
|
3002
3032
|
* more than counting in place.
|
|
3003
3033
|
*/
|
|
3004
3034
|
function createCounts(source, name) {
|
|
3005
|
-
const counts =
|
|
3006
|
-
if (source) for (const key of Object.keys(source)) {
|
|
3007
|
-
const value = source[key];
|
|
3008
|
-
if (isCount(value)) counts[key] = value;
|
|
3009
|
-
}
|
|
3035
|
+
const counts = ownEntries([source], isCount);
|
|
3010
3036
|
if (name !== void 0) counts[name] = countOf(source, name) + 1;
|
|
3011
3037
|
return counts;
|
|
3012
3038
|
}
|
|
@@ -3045,7 +3071,8 @@ function transform(expr, goal = "expression") {
|
|
|
3045
3071
|
result += text;
|
|
3046
3072
|
},
|
|
3047
3073
|
variable(sigil, name) {
|
|
3048
|
-
|
|
3074
|
+
const error = variableNameError(name, sigil + name);
|
|
3075
|
+
if (error) throw new SyntaxError(`spindle: ${error}`);
|
|
3049
3076
|
if (IDENT_END_RE.test(result.slice(-2))) result += " ";
|
|
3050
3077
|
result += `${NAMESPACES$1[sigil]}["${name}"]`;
|
|
3051
3078
|
}
|
|
@@ -3068,41 +3095,50 @@ function getOrCompile(key, body) {
|
|
|
3068
3095
|
}
|
|
3069
3096
|
return fn;
|
|
3070
3097
|
}
|
|
3098
|
+
/** The passage being shown. */
|
|
3099
|
+
function currentPassage() {
|
|
3100
|
+
const s = useStoryStore.getState();
|
|
3101
|
+
return s.storyData?.passages.get(s.currentPassage);
|
|
3102
|
+
}
|
|
3103
|
+
/** The passage shown before the current one in history. */
|
|
3104
|
+
function previousPassage() {
|
|
3105
|
+
const s = useStoryStore.getState();
|
|
3106
|
+
if (s.historyIndex <= 0) return void 0;
|
|
3107
|
+
const prevName = s.history[s.historyIndex - 1]?.passage;
|
|
3108
|
+
return prevName ? s.storyData?.passages.get(prevName) : void 0;
|
|
3109
|
+
}
|
|
3110
|
+
/**
|
|
3111
|
+
* visited()/rendered() and their has-forms, counting in the counters
|
|
3112
|
+
* `counts()` returns. A missing name means the current passage.
|
|
3113
|
+
*/
|
|
3114
|
+
function countQueries(counts) {
|
|
3115
|
+
const count = (name) => countOf(counts(), name ?? useStoryStore.getState().currentPassage);
|
|
3116
|
+
return {
|
|
3117
|
+
count,
|
|
3118
|
+
has: (name) => count(name) > 0,
|
|
3119
|
+
any: (...names) => names.some((n) => count(n) > 0),
|
|
3120
|
+
all: (...names) => names.every((n) => count(n) > 0)
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3071
3123
|
var cachedFns = null;
|
|
3072
3124
|
var cachedVisitCounts = null;
|
|
3073
3125
|
var cachedRenderCounts = null;
|
|
3074
3126
|
function buildExpressionFns() {
|
|
3075
3127
|
const { visitCounts, renderCounts } = useStoryStore.getState();
|
|
3076
3128
|
if (cachedFns && cachedVisitCounts === visitCounts && cachedRenderCounts === renderCounts) return cachedFns;
|
|
3077
|
-
const
|
|
3078
|
-
const
|
|
3079
|
-
const hasVisitedAny = (...names) => names.some((n) => visited(n) > 0);
|
|
3080
|
-
const hasVisitedAll = (...names) => names.every((n) => visited(n) > 0);
|
|
3081
|
-
const rendered = (name) => countOf(renderCounts, name ?? useStoryStore.getState().currentPassage);
|
|
3082
|
-
const hasRendered = (name) => rendered(name) > 0;
|
|
3083
|
-
const hasRenderedAny = (...names) => names.some((n) => rendered(n) > 0);
|
|
3084
|
-
const hasRenderedAll = (...names) => names.every((n) => rendered(n) > 0);
|
|
3085
|
-
const currentPassage = () => {
|
|
3086
|
-
const s = useStoryStore.getState();
|
|
3087
|
-
return s.storyData?.passages.get(s.currentPassage);
|
|
3088
|
-
};
|
|
3089
|
-
const previousPassage = () => {
|
|
3090
|
-
const s = useStoryStore.getState();
|
|
3091
|
-
if (s.historyIndex <= 0) return void 0;
|
|
3092
|
-
const prevName = s.history[s.historyIndex - 1]?.passage;
|
|
3093
|
-
return prevName ? s.storyData?.passages.get(prevName) : void 0;
|
|
3094
|
-
};
|
|
3129
|
+
const visits = countQueries(() => visitCounts);
|
|
3130
|
+
const renders = countQueries(() => renderCounts);
|
|
3095
3131
|
cachedFns = {
|
|
3096
3132
|
currentPassage,
|
|
3097
3133
|
previousPassage,
|
|
3098
|
-
visited,
|
|
3099
|
-
hasVisited,
|
|
3100
|
-
hasVisitedAny,
|
|
3101
|
-
hasVisitedAll,
|
|
3102
|
-
rendered,
|
|
3103
|
-
hasRendered,
|
|
3104
|
-
hasRenderedAny,
|
|
3105
|
-
hasRenderedAll,
|
|
3134
|
+
visited: visits.count,
|
|
3135
|
+
hasVisited: visits.has,
|
|
3136
|
+
hasVisitedAny: visits.any,
|
|
3137
|
+
hasVisitedAll: visits.all,
|
|
3138
|
+
rendered: renders.count,
|
|
3139
|
+
hasRendered: renders.has,
|
|
3140
|
+
hasRenderedAny: renders.any,
|
|
3141
|
+
hasRenderedAll: renders.all,
|
|
3106
3142
|
random,
|
|
3107
3143
|
randomInt
|
|
3108
3144
|
};
|
|
@@ -3700,6 +3736,11 @@ var SIGILS = {
|
|
|
3700
3736
|
local: "@",
|
|
3701
3737
|
transient: "%"
|
|
3702
3738
|
};
|
|
3739
|
+
var STORE_NAMESPACES = {
|
|
3740
|
+
variable: "variables",
|
|
3741
|
+
temporary: "temporary",
|
|
3742
|
+
transient: "transient"
|
|
3743
|
+
};
|
|
3703
3744
|
function VarDisplay({ name, scope, className, id }) {
|
|
3704
3745
|
const resolve = useInterpolate();
|
|
3705
3746
|
className = resolve(className);
|
|
@@ -3707,10 +3748,11 @@ function VarDisplay({ name, scope, className, id }) {
|
|
|
3707
3748
|
const localsValues = useContext(LocalsValuesContext);
|
|
3708
3749
|
const parts = name.split(".");
|
|
3709
3750
|
const root = parts[0];
|
|
3710
|
-
const storeValue = useStoryStore((s) => scope === "
|
|
3711
|
-
|
|
3751
|
+
const storeValue = useStoryStore((s) => scope === "local" ? void 0 : ownValue(s[STORE_NAMESPACES[scope]], root));
|
|
3752
|
+
const nameError = variableNameError(root, SIGILS[scope] + root);
|
|
3753
|
+
if (nameError) return /* @__PURE__ */ jsx("span", {
|
|
3712
3754
|
class: "error",
|
|
3713
|
-
children: `{${SIGILS[scope]}${name} error:
|
|
3755
|
+
children: `{${SIGILS[scope]}${name} error: ${nameError}}`
|
|
3714
3756
|
});
|
|
3715
3757
|
let value;
|
|
3716
3758
|
if (scope === "local") value = ownValue(localsValues, root);
|
|
@@ -3737,10 +3779,7 @@ function ExprDisplay({ expression, className, id }) {
|
|
|
3737
3779
|
const resolve = useInterpolate();
|
|
3738
3780
|
className = resolve(className);
|
|
3739
3781
|
id = resolve(id);
|
|
3740
|
-
const localsValues =
|
|
3741
|
-
const variables = useStoryStore((s) => s.variables);
|
|
3742
|
-
const temporary = useStoryStore((s) => s.temporary);
|
|
3743
|
-
const transient = useStoryStore((s) => s.transient);
|
|
3782
|
+
const [variables, temporary, localsValues, transient] = useMergedLocals();
|
|
3744
3783
|
let display;
|
|
3745
3784
|
try {
|
|
3746
3785
|
const value = evaluate(expression, variables, temporary, localsValues, transient);
|
|
@@ -5049,10 +5088,14 @@ function on(event, cb) {
|
|
|
5049
5088
|
set.delete(cb);
|
|
5050
5089
|
};
|
|
5051
5090
|
}
|
|
5052
|
-
|
|
5091
|
+
/** Call `call` with each listener of `event`. */
|
|
5092
|
+
function forEachListener(event, call) {
|
|
5053
5093
|
const set = listeners.get(event);
|
|
5054
5094
|
if (!set) return;
|
|
5055
|
-
for (const cb of [...set]) cb
|
|
5095
|
+
for (const cb of [...set]) call(cb);
|
|
5096
|
+
}
|
|
5097
|
+
function emit(event, ...args) {
|
|
5098
|
+
forEachListener(event, (cb) => cb(...args));
|
|
5056
5099
|
}
|
|
5057
5100
|
/**
|
|
5058
5101
|
* Emit an event from inside a render commit (layout effect). A throwing
|
|
@@ -5060,13 +5103,13 @@ function emit(event, ...args) {
|
|
|
5060
5103
|
* render; the remaining handlers still run.
|
|
5061
5104
|
*/
|
|
5062
5105
|
function emitFromRender(event, ...args) {
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
}
|
|
5106
|
+
forEachListener(event, (cb) => {
|
|
5107
|
+
try {
|
|
5108
|
+
cb(...args);
|
|
5109
|
+
} catch (err) {
|
|
5110
|
+
console.error(`spindle: Error in ${event} handler:`, err);
|
|
5111
|
+
}
|
|
5112
|
+
});
|
|
5070
5113
|
}
|
|
5071
5114
|
//#endregion
|
|
5072
5115
|
//#region src/saves/save-manager.ts
|
|
@@ -5784,7 +5827,6 @@ function initializeStory(session) {
|
|
|
5784
5827
|
}
|
|
5785
5828
|
//#endregion
|
|
5786
5829
|
//#region src/utils/object-path.ts
|
|
5787
|
-
var hasOwn$2 = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
5788
5830
|
/**
|
|
5789
5831
|
* Traverse dot-path segments on an object and return the nested value.
|
|
5790
5832
|
* Members of Object.prototype (`constructor`, `toString`, `__proto__`, ...)
|
|
@@ -7638,9 +7680,7 @@ function TriggerDialogHost() {
|
|
|
7638
7680
|
//#endregion
|
|
7639
7681
|
//#region src/components/App.tsx
|
|
7640
7682
|
function App() {
|
|
7641
|
-
const storyData =
|
|
7642
|
-
const currentPassage = useStoryStore((s) => s.currentPassage);
|
|
7643
|
-
const nobr = useStoryStore((s) => s.nobr);
|
|
7683
|
+
const { storyData, currentPassage, nobr } = useStoryFields("storyData", "currentPassage", "nobr");
|
|
7644
7684
|
useEffect(() => {
|
|
7645
7685
|
const onKeyDown = (e) => {
|
|
7646
7686
|
const { quickSaveKey, quickLoadKey, save, load } = useStoryStore.getState();
|
|
@@ -7759,45 +7799,47 @@ function loadFromStorage() {
|
|
|
7759
7799
|
}
|
|
7760
7800
|
} catch {}
|
|
7761
7801
|
}
|
|
7802
|
+
/** Register a setting, starting from its default unless it has a value. */
|
|
7803
|
+
function define(name, def) {
|
|
7804
|
+
definitions.set(name, def);
|
|
7805
|
+
if (!hasOwn$2(values, name)) values[name] = def.config.default;
|
|
7806
|
+
loadFromStorage();
|
|
7807
|
+
}
|
|
7808
|
+
/** The value of setting `name` if it has the type of `fallback`, else that. */
|
|
7809
|
+
function valueOf(name, fallback) {
|
|
7810
|
+
const v = ownValue(values, name);
|
|
7811
|
+
return typeof v === typeof fallback ? v : fallback;
|
|
7812
|
+
}
|
|
7762
7813
|
var settings = {
|
|
7763
7814
|
addToggle(name, config) {
|
|
7764
|
-
|
|
7815
|
+
define(name, {
|
|
7765
7816
|
type: "toggle",
|
|
7766
7817
|
config
|
|
7767
7818
|
});
|
|
7768
|
-
if (!Object.prototype.hasOwnProperty.call(values, name)) values[name] = config.default;
|
|
7769
|
-
loadFromStorage();
|
|
7770
7819
|
},
|
|
7771
7820
|
addList(name, config) {
|
|
7772
|
-
|
|
7821
|
+
define(name, {
|
|
7773
7822
|
type: "list",
|
|
7774
7823
|
config
|
|
7775
7824
|
});
|
|
7776
|
-
if (!Object.prototype.hasOwnProperty.call(values, name)) values[name] = config.default;
|
|
7777
|
-
loadFromStorage();
|
|
7778
7825
|
},
|
|
7779
7826
|
addRange(name, config) {
|
|
7780
|
-
|
|
7827
|
+
define(name, {
|
|
7781
7828
|
type: "range",
|
|
7782
7829
|
config
|
|
7783
7830
|
});
|
|
7784
|
-
if (!Object.prototype.hasOwnProperty.call(values, name)) values[name] = config.default;
|
|
7785
|
-
loadFromStorage();
|
|
7786
7831
|
},
|
|
7787
7832
|
get(name) {
|
|
7788
7833
|
return ownValue(values, name);
|
|
7789
7834
|
},
|
|
7790
7835
|
getToggle(name) {
|
|
7791
|
-
|
|
7792
|
-
return typeof v === "boolean" ? v : false;
|
|
7836
|
+
return valueOf(name, false);
|
|
7793
7837
|
},
|
|
7794
7838
|
getList(name) {
|
|
7795
|
-
|
|
7796
|
-
return typeof v === "string" ? v : "";
|
|
7839
|
+
return valueOf(name, "");
|
|
7797
7840
|
},
|
|
7798
7841
|
getRange(name) {
|
|
7799
|
-
|
|
7800
|
-
return typeof v === "number" ? v : 0;
|
|
7842
|
+
return valueOf(name, 0);
|
|
7801
7843
|
},
|
|
7802
7844
|
set(name, value) {
|
|
7803
7845
|
values[name] = value;
|
|
@@ -8096,26 +8138,17 @@ function ensureVariableChangedSubscription() {
|
|
|
8096
8138
|
let prevTrans = useStoryStore.getState().transient;
|
|
8097
8139
|
useStoryStore.subscribe((state) => {
|
|
8098
8140
|
const changed = {};
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
from: prevVars[key],
|
|
8104
|
-
to: state.variables[key]
|
|
8105
|
-
};
|
|
8106
|
-
hasChanges = true;
|
|
8107
|
-
}
|
|
8108
|
-
const allTransKeys = new Set([...Object.keys(prevTrans), ...Object.keys(state.transient)]);
|
|
8109
|
-
for (const key of allTransKeys) if (state.transient[key] !== prevTrans[key]) {
|
|
8110
|
-
changed[`%${key}`] = {
|
|
8111
|
-
from: prevTrans[key],
|
|
8112
|
-
to: state.transient[key]
|
|
8141
|
+
const collect = (prev, next, prefix) => {
|
|
8142
|
+
for (const key of changedNames(prev, next)) changed[prefix + key] = {
|
|
8143
|
+
from: ownValue(prev, key),
|
|
8144
|
+
to: ownValue(next, key)
|
|
8113
8145
|
};
|
|
8114
|
-
|
|
8115
|
-
|
|
8146
|
+
};
|
|
8147
|
+
collect(prevVars, state.variables, "");
|
|
8148
|
+
collect(prevTrans, state.transient, "%");
|
|
8116
8149
|
prevVars = state.variables;
|
|
8117
8150
|
prevTrans = state.transient;
|
|
8118
|
-
if (
|
|
8151
|
+
if (Object.keys(changed).length > 0) emit("variableChanged", changed);
|
|
8119
8152
|
});
|
|
8120
8153
|
}
|
|
8121
8154
|
var declaredVariables = null;
|
|
@@ -8160,13 +8193,15 @@ function setOne(draft, name, value) {
|
|
|
8160
8193
|
else namespace[key] = value;
|
|
8161
8194
|
}
|
|
8162
8195
|
function createStoryAPI() {
|
|
8196
|
+
const visits = countQueries(() => useStoryStore.getState().visitCounts);
|
|
8197
|
+
const renders = countQueries(() => useStoryStore.getState().renderCounts);
|
|
8163
8198
|
return {
|
|
8164
8199
|
get(name) {
|
|
8165
8200
|
const { isTransient, key } = parseName(name);
|
|
8166
8201
|
const scope = getActiveMutationScope();
|
|
8167
8202
|
const source = scope ?? useStoryStore.getState();
|
|
8168
8203
|
const namespace = isTransient ? source.transient : source.variables;
|
|
8169
|
-
const value = key.includes(".") ? getByPath(namespace, key.split(".")) : namespace
|
|
8204
|
+
const value = key.includes(".") ? getByPath(namespace, key.split(".")) : ownValue(namespace, key);
|
|
8170
8205
|
return scope ? frozenCopy(value) : value;
|
|
8171
8206
|
},
|
|
8172
8207
|
set(nameOrVars, value) {
|
|
@@ -8215,46 +8250,16 @@ function createStoryAPI() {
|
|
|
8215
8250
|
importSave(data, slot) {
|
|
8216
8251
|
return useStoryStore.getState().importSave(data, slot);
|
|
8217
8252
|
},
|
|
8218
|
-
visited
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
},
|
|
8229
|
-
hasVisitedAll(...names) {
|
|
8230
|
-
const { visitCounts } = useStoryStore.getState();
|
|
8231
|
-
return names.every((n) => countOf(visitCounts, n) > 0);
|
|
8232
|
-
},
|
|
8233
|
-
rendered(name) {
|
|
8234
|
-
const state = useStoryStore.getState();
|
|
8235
|
-
return countOf(state.renderCounts, name ?? state.currentPassage);
|
|
8236
|
-
},
|
|
8237
|
-
hasRendered(name) {
|
|
8238
|
-
return this.rendered(name) > 0;
|
|
8239
|
-
},
|
|
8240
|
-
hasRenderedAny(...names) {
|
|
8241
|
-
const { renderCounts } = useStoryStore.getState();
|
|
8242
|
-
return names.some((n) => countOf(renderCounts, n) > 0);
|
|
8243
|
-
},
|
|
8244
|
-
hasRenderedAll(...names) {
|
|
8245
|
-
const { renderCounts } = useStoryStore.getState();
|
|
8246
|
-
return names.every((n) => countOf(renderCounts, n) > 0);
|
|
8247
|
-
},
|
|
8248
|
-
currentPassage() {
|
|
8249
|
-
const state = useStoryStore.getState();
|
|
8250
|
-
return state.storyData?.passages.get(state.currentPassage);
|
|
8251
|
-
},
|
|
8252
|
-
previousPassage() {
|
|
8253
|
-
const state = useStoryStore.getState();
|
|
8254
|
-
if (state.historyIndex <= 0) return void 0;
|
|
8255
|
-
const prevName = state.history[state.historyIndex - 1]?.passage;
|
|
8256
|
-
return prevName ? state.storyData?.passages.get(prevName) : void 0;
|
|
8257
|
-
},
|
|
8253
|
+
visited: visits.count,
|
|
8254
|
+
hasVisited: visits.has,
|
|
8255
|
+
hasVisitedAny: visits.any,
|
|
8256
|
+
hasVisitedAll: visits.all,
|
|
8257
|
+
rendered: renders.count,
|
|
8258
|
+
hasRendered: renders.has,
|
|
8259
|
+
hasRenderedAny: renders.any,
|
|
8260
|
+
hasRenderedAll: renders.all,
|
|
8261
|
+
currentPassage,
|
|
8262
|
+
previousPassage,
|
|
8258
8263
|
get title() {
|
|
8259
8264
|
return useStoryStore.getState().storyData?.name || "";
|
|
8260
8265
|
},
|
|
@@ -8492,7 +8497,8 @@ function parseStoryVariables(content, sigil = "$") {
|
|
|
8492
8497
|
const match = line.match(DECLARATION_RE);
|
|
8493
8498
|
if (!match) throw new Error(`${passageName}: Invalid declaration: "${line}". Expected: ${sigil}name = value`);
|
|
8494
8499
|
const [, name, expr] = match;
|
|
8495
|
-
|
|
8500
|
+
const nameError = variableNameError(name, sigil + name);
|
|
8501
|
+
if (nameError) throw new Error(`${passageName}: ${nameError}`);
|
|
8496
8502
|
let value;
|
|
8497
8503
|
try {
|
|
8498
8504
|
value = new Function("return (" + expr + ")")();
|
|
@@ -8535,7 +8541,8 @@ function extractForLocals(content) {
|
|
|
8535
8541
|
function validateRef(ref, schema, forLocals) {
|
|
8536
8542
|
const parts = ref.split(".");
|
|
8537
8543
|
const rootName = parts[0];
|
|
8538
|
-
|
|
8544
|
+
const nameError = variableNameError(rootName, "$" + rootName);
|
|
8545
|
+
if (nameError) return nameError;
|
|
8539
8546
|
if (forLocals.has(rootName)) return null;
|
|
8540
8547
|
const rootSchema = schema.get(rootName);
|
|
8541
8548
|
if (!rootSchema) return `Undeclared variable: $${ref}`;
|
|
@@ -9159,7 +9166,7 @@ defineMacro({
|
|
|
9159
9166
|
function liveLocalsView(getValues) {
|
|
9160
9167
|
return new Proxy(createNamespace(), {
|
|
9161
9168
|
get: (_, key) => typeof key === "string" ? ownValue(getValues(), key) : void 0,
|
|
9162
|
-
has: (_, key) => typeof key === "string" &&
|
|
9169
|
+
has: (_, key) => typeof key === "string" && hasOwn$2(getValues(), key),
|
|
9163
9170
|
ownKeys: () => Reflect.ownKeys(getValues()),
|
|
9164
9171
|
getOwnPropertyDescriptor: (_, key) => {
|
|
9165
9172
|
const desc = Object.getOwnPropertyDescriptor(getValues(), key);
|
|
@@ -9631,10 +9638,13 @@ defineMenubarAction({
|
|
|
9631
9638
|
name: "quicksave",
|
|
9632
9639
|
label: "QuickSave",
|
|
9633
9640
|
actionType: "save",
|
|
9634
|
-
setup: () =>
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9641
|
+
setup: () => {
|
|
9642
|
+
const { save, quickSaveKey } = useStoryFields("save", "quickSaveKey");
|
|
9643
|
+
return {
|
|
9644
|
+
perform: save,
|
|
9645
|
+
title: hotkeyTitle("Quick Save", quickSaveKey)
|
|
9646
|
+
};
|
|
9647
|
+
}
|
|
9638
9648
|
});
|
|
9639
9649
|
defineMenubarAction({
|
|
9640
9650
|
name: "quickload",
|
|
@@ -9642,14 +9652,11 @@ defineMenubarAction({
|
|
|
9642
9652
|
actionType: "load",
|
|
9643
9653
|
confirm: "Load saved game? Current progress will be lost.",
|
|
9644
9654
|
setup: () => {
|
|
9645
|
-
const load =
|
|
9646
|
-
const hasSave = useStoryStore((s) => s.hasSave);
|
|
9647
|
-
const key = useStoryStore((s) => s.quickLoadKey);
|
|
9648
|
-
useStoryStore((s) => s.knownSaves);
|
|
9655
|
+
const { load, hasSave, quickLoadKey } = useStoryFields("load", "hasSave", "quickLoadKey", "knownSaves");
|
|
9649
9656
|
return {
|
|
9650
9657
|
perform: () => load(),
|
|
9651
9658
|
disabled: !hasSave(),
|
|
9652
|
-
title: hotkeyTitle("Quick Load",
|
|
9659
|
+
title: hotkeyTitle("Quick Load", quickLoadKey)
|
|
9653
9660
|
};
|
|
9654
9661
|
}
|
|
9655
9662
|
});
|
|
@@ -9737,10 +9744,7 @@ function SaveManagerContent() {
|
|
|
9737
9744
|
const renameInputRef = useRef(null);
|
|
9738
9745
|
const fileInputRef = useRef(null);
|
|
9739
9746
|
const closeDialog = useContext(DialogCloseContext);
|
|
9740
|
-
const storyData =
|
|
9741
|
-
const playthroughId = useStoryStore((s) => s.playthroughId);
|
|
9742
|
-
const beginSave = useStoryStore((s) => s.beginSave);
|
|
9743
|
-
const loadFromPayload = useStoryStore((s) => s.loadFromPayload);
|
|
9747
|
+
const { storyData, playthroughId, beginSave, loadFromPayload } = useStoryFields("storyData", "playthroughId", "beginSave", "loadFromPayload");
|
|
9744
9748
|
const ifid = storyData?.ifid ?? "";
|
|
9745
9749
|
const refresh = useCallback(async () => {
|
|
9746
9750
|
if (!ifid) return;
|
|
@@ -10680,10 +10684,7 @@ defineMacro({
|
|
|
10680
10684
|
name: "passage",
|
|
10681
10685
|
interpolate: true,
|
|
10682
10686
|
render(_props, ctx) {
|
|
10683
|
-
const currentPassage =
|
|
10684
|
-
const navigationId = useStoryStore((s) => s.navigationId);
|
|
10685
|
-
const storyData = useStoryStore((s) => s.storyData);
|
|
10686
|
-
const renderDeferred = useStoryStore((s) => s.renderDeferred);
|
|
10687
|
+
const { currentPassage, navigationId, storyData, renderDeferred } = useStoryFields("currentPassage", "navigationId", "storyData", "renderDeferred");
|
|
10687
10688
|
const [displayed, setDisplayed] = useState({
|
|
10688
10689
|
passage: currentPassage,
|
|
10689
10690
|
id: navigationId
|
|
@@ -2234,15 +2234,30 @@ function errorMessage(err, fallback) {
|
|
|
2234
2234
|
//#region src/utils/namespace.ts
|
|
2235
2235
|
/** The variable name no namespace can hold. */
|
|
2236
2236
|
var RESERVED_NAME = "__proto__";
|
|
2237
|
-
/**
|
|
2238
|
-
|
|
2239
|
-
|
|
2237
|
+
/**
|
|
2238
|
+
* Why `name` (without its sigil) cannot be a variable name, or undefined if
|
|
2239
|
+
* it can. `label` is the name as the author wrote it (e.g. `$__proto__`).
|
|
2240
|
+
*/
|
|
2241
|
+
function variableNameError(name, label = name) {
|
|
2242
|
+
return name === "__proto__" ? `"${label}" cannot be used as a variable name (${RESERVED_NAME} is reserved)` : void 0;
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* A record with no prototype holding the own entries of `sources`, or only
|
|
2246
|
+
* those whose value passes `keep`.
|
|
2247
|
+
*/
|
|
2248
|
+
function ownEntries(sources, keep = () => true) {
|
|
2249
|
+
const record = Object.create(null);
|
|
2240
2250
|
for (const source of sources) {
|
|
2241
2251
|
if (!source) continue;
|
|
2242
|
-
for (const key of Object.keys(source))
|
|
2252
|
+
for (const key of Object.keys(source)) {
|
|
2253
|
+
const value = source[key];
|
|
2254
|
+
if (keep(value)) record[key] = value;
|
|
2255
|
+
}
|
|
2243
2256
|
}
|
|
2244
|
-
return
|
|
2257
|
+
return record;
|
|
2245
2258
|
}
|
|
2259
|
+
/** A namespace with no prototype holding the own entries of `sources`. */
|
|
2260
|
+
var createNamespace = (...sources) => ownEntries(sources);
|
|
2246
2261
|
Object.freeze(createNamespace());
|
|
2247
2262
|
//#endregion
|
|
2248
2263
|
//#region src/story-variables.ts
|
|
@@ -2294,7 +2309,8 @@ function parseStoryVariables(content, sigil = "$") {
|
|
|
2294
2309
|
const match = line.match(DECLARATION_RE);
|
|
2295
2310
|
if (!match) throw new Error(`${passageName}: Invalid declaration: "${line}". Expected: ${sigil}name = value`);
|
|
2296
2311
|
const [, name, expr] = match;
|
|
2297
|
-
|
|
2312
|
+
const nameError = variableNameError(name, sigil + name);
|
|
2313
|
+
if (nameError) throw new Error(`${passageName}: ${nameError}`);
|
|
2298
2314
|
let value;
|
|
2299
2315
|
try {
|
|
2300
2316
|
value = new Function("return (" + expr + ")")();
|
|
@@ -2337,7 +2353,8 @@ function extractForLocals(content) {
|
|
|
2337
2353
|
function validateRef(ref, schema, forLocals) {
|
|
2338
2354
|
const parts = ref.split(".");
|
|
2339
2355
|
const rootName = parts[0];
|
|
2340
|
-
|
|
2356
|
+
const nameError = variableNameError(rootName, "$" + rootName);
|
|
2357
|
+
if (nameError) return nameError;
|
|
2341
2358
|
if (forLocals.has(rootName)) return null;
|
|
2342
2359
|
const rootSchema = schema.get(rootName);
|
|
2343
2360
|
if (!rootSchema) return `Undeclared variable: $${ref}`;
|
package/package.json
CHANGED
package/src/components/App.tsx
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { useEffect } from 'preact/hooks';
|
|
2
2
|
import { useStoryStore } from '../store';
|
|
3
|
+
import { useStoryFields } from '../hooks/use-story-fields';
|
|
3
4
|
import { NobrContext } from '../markup/render';
|
|
4
5
|
import { StoryInterface } from './StoryInterface';
|
|
5
6
|
import { TriggerDialogHost } from './TriggerDialogHost';
|
|
6
7
|
|
|
7
8
|
export function App() {
|
|
8
|
-
const storyData =
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const { storyData, currentPassage, nobr } = useStoryFields(
|
|
10
|
+
'storyData',
|
|
11
|
+
'currentPassage',
|
|
12
|
+
'nobr',
|
|
13
|
+
);
|
|
11
14
|
|
|
12
15
|
useEffect(() => {
|
|
13
16
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { useContext } from 'preact/hooks';
|
|
2
|
-
import { useStoryStore } from '../../store';
|
|
3
1
|
import { evaluate } from '../../expression';
|
|
4
|
-
import { LocalsValuesContext } from '../../markup/render';
|
|
5
2
|
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
3
|
+
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
6
4
|
|
|
7
5
|
interface ExprDisplayProps {
|
|
8
6
|
expression: string;
|
|
@@ -14,10 +12,7 @@ export function ExprDisplay({ expression, className, id }: ExprDisplayProps) {
|
|
|
14
12
|
const resolve = useInterpolate();
|
|
15
13
|
className = resolve(className);
|
|
16
14
|
id = resolve(id);
|
|
17
|
-
const localsValues =
|
|
18
|
-
const variables = useStoryStore((s) => s.variables);
|
|
19
|
-
const temporary = useStoryStore((s) => s.temporary);
|
|
20
|
-
const transient = useStoryStore((s) => s.transient);
|
|
15
|
+
const [variables, temporary, localsValues, transient] = useMergedLocals();
|
|
21
16
|
|
|
22
17
|
let display: string;
|
|
23
18
|
try {
|