@rohal12/spindle 0.1.0 → 0.2.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.
- package/dist/pkg/format.js +1 -1
- package/package.json +3 -2
- package/src/class-registry.ts +189 -0
- package/src/components/StoryInterface.tsx +2 -1
- package/src/components/macros/Button.tsx +3 -2
- package/src/components/macros/Cycle.tsx +1 -1
- package/src/components/macros/Do.tsx +3 -2
- package/src/components/macros/Listbox.tsx +1 -3
- package/src/components/macros/MacroLink.tsx +3 -2
- package/src/components/macros/Meter.tsx +130 -0
- package/src/components/macros/Numberbox.tsx +1 -3
- package/src/components/macros/Set.tsx +3 -2
- package/src/components/macros/Textarea.tsx +1 -3
- package/src/components/macros/Textbox.tsx +1 -3
- package/src/components/macros/Type.tsx +2 -9
- package/src/index.tsx +1 -5
- package/src/markup/ast.ts +3 -6
- package/src/markup/markdown.ts +1 -4
- package/src/markup/render.tsx +13 -6
- package/src/markup/tokenizer.ts +66 -8
- package/src/saves/save-manager.ts +24 -4
- package/src/store.ts +17 -13
- package/src/story-api.ts +6 -0
- package/src/story-init.ts +3 -2
- package/src/styles.css +31 -1
package/src/markup/tokenizer.ts
CHANGED
|
@@ -46,16 +46,74 @@ export interface HtmlToken {
|
|
|
46
46
|
end: number;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export type Token =
|
|
49
|
+
export type Token =
|
|
50
|
+
| TextToken
|
|
51
|
+
| LinkToken
|
|
52
|
+
| MacroToken
|
|
53
|
+
| VariableToken
|
|
54
|
+
| HtmlToken;
|
|
50
55
|
|
|
51
56
|
const HTML_TAGS = new Set([
|
|
52
|
-
'a',
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'
|
|
57
|
+
'a',
|
|
58
|
+
'article',
|
|
59
|
+
'aside',
|
|
60
|
+
'b',
|
|
61
|
+
'blockquote',
|
|
62
|
+
'br',
|
|
63
|
+
'caption',
|
|
64
|
+
'code',
|
|
65
|
+
'col',
|
|
66
|
+
'colgroup',
|
|
67
|
+
'dd',
|
|
68
|
+
'del',
|
|
69
|
+
'details',
|
|
70
|
+
'dfn',
|
|
71
|
+
'div',
|
|
72
|
+
'dl',
|
|
73
|
+
'dt',
|
|
74
|
+
'em',
|
|
75
|
+
'figcaption',
|
|
76
|
+
'figure',
|
|
77
|
+
'footer',
|
|
78
|
+
'h1',
|
|
79
|
+
'h2',
|
|
80
|
+
'h3',
|
|
81
|
+
'h4',
|
|
82
|
+
'h5',
|
|
83
|
+
'h6',
|
|
84
|
+
'header',
|
|
85
|
+
'hr',
|
|
86
|
+
'i',
|
|
87
|
+
'img',
|
|
88
|
+
'ins',
|
|
89
|
+
'kbd',
|
|
90
|
+
'li',
|
|
91
|
+
'main',
|
|
92
|
+
'mark',
|
|
93
|
+
'nav',
|
|
94
|
+
'ol',
|
|
95
|
+
'p',
|
|
96
|
+
'pre',
|
|
97
|
+
'q',
|
|
98
|
+
's',
|
|
99
|
+
'samp',
|
|
100
|
+
'section',
|
|
101
|
+
'small',
|
|
102
|
+
'span',
|
|
103
|
+
'strong',
|
|
104
|
+
'sub',
|
|
105
|
+
'summary',
|
|
106
|
+
'sup',
|
|
107
|
+
'table',
|
|
108
|
+
'tbody',
|
|
109
|
+
'td',
|
|
110
|
+
'tfoot',
|
|
111
|
+
'th',
|
|
112
|
+
'thead',
|
|
113
|
+
'tr',
|
|
114
|
+
'u',
|
|
115
|
+
'ul',
|
|
116
|
+
'wbr',
|
|
59
117
|
]);
|
|
60
118
|
|
|
61
119
|
const HTML_VOID_TAGS = new Set(['br', 'col', 'hr', 'img', 'wbr']);
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getMeta,
|
|
16
16
|
setMeta,
|
|
17
17
|
} from './idb';
|
|
18
|
+
import { deepClone, serialize, deserialize } from '../class-registry';
|
|
18
19
|
|
|
19
20
|
type TitleGenerator = (payload: SavePayload) => string;
|
|
20
21
|
|
|
@@ -118,7 +119,13 @@ export async function createSave(
|
|
|
118
119
|
custom,
|
|
119
120
|
};
|
|
120
121
|
|
|
121
|
-
const
|
|
122
|
+
const serializedPayload = deepClone(payload);
|
|
123
|
+
serializedPayload.variables = serialize(serializedPayload.variables);
|
|
124
|
+
serializedPayload.history = serializedPayload.history.map((m) => ({
|
|
125
|
+
...m,
|
|
126
|
+
variables: serialize(m.variables),
|
|
127
|
+
}));
|
|
128
|
+
const record: SaveRecord = { meta, payload: serializedPayload };
|
|
122
129
|
await putSave(record);
|
|
123
130
|
return record;
|
|
124
131
|
}
|
|
@@ -132,7 +139,13 @@ export async function overwriteSave(
|
|
|
132
139
|
|
|
133
140
|
existing.meta.updatedAt = new Date().toISOString();
|
|
134
141
|
existing.meta.passage = payload.passage;
|
|
135
|
-
|
|
142
|
+
const serializedPayload = deepClone(payload);
|
|
143
|
+
serializedPayload.variables = serialize(serializedPayload.variables);
|
|
144
|
+
serializedPayload.history = serializedPayload.history.map((m) => ({
|
|
145
|
+
...m,
|
|
146
|
+
variables: serialize(m.variables),
|
|
147
|
+
}));
|
|
148
|
+
existing.payload = serializedPayload;
|
|
136
149
|
await putSave(existing);
|
|
137
150
|
return existing;
|
|
138
151
|
}
|
|
@@ -141,7 +154,14 @@ export async function loadSave(
|
|
|
141
154
|
saveId: string,
|
|
142
155
|
): Promise<SavePayload | undefined> {
|
|
143
156
|
const record = await getSave(saveId);
|
|
144
|
-
return
|
|
157
|
+
if (!record) return undefined;
|
|
158
|
+
const payload = record.payload;
|
|
159
|
+
payload.variables = deserialize(payload.variables);
|
|
160
|
+
payload.history = payload.history.map((m) => ({
|
|
161
|
+
...m,
|
|
162
|
+
variables: deserialize(m.variables),
|
|
163
|
+
}));
|
|
164
|
+
return payload;
|
|
145
165
|
}
|
|
146
166
|
|
|
147
167
|
export async function deleteSaveById(saveId: string): Promise<void> {
|
|
@@ -294,7 +314,7 @@ export async function importSave(
|
|
|
294
314
|
}
|
|
295
315
|
|
|
296
316
|
// Re-assign a new ID to avoid collisions
|
|
297
|
-
const record =
|
|
317
|
+
const record = deepClone(data.save);
|
|
298
318
|
record.meta.id = crypto.randomUUID();
|
|
299
319
|
record.meta.updatedAt = new Date().toISOString();
|
|
300
320
|
|
package/src/store.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
quickSave,
|
|
11
11
|
loadQuickSave,
|
|
12
12
|
} from './saves/save-manager';
|
|
13
|
+
import { deepClone, deserialize } from './class-registry';
|
|
13
14
|
|
|
14
15
|
export interface HistoryMoment {
|
|
15
16
|
passage: string;
|
|
@@ -75,7 +76,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
75
76
|
);
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
const initialVars =
|
|
79
|
+
const initialVars = deepClone(variableDefaults);
|
|
79
80
|
|
|
80
81
|
set((state) => {
|
|
81
82
|
state.storyData = storyData as StoryData;
|
|
@@ -86,7 +87,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
86
87
|
state.history = [
|
|
87
88
|
{
|
|
88
89
|
passage: startPassage.name,
|
|
89
|
-
variables:
|
|
90
|
+
variables: deepClone(initialVars),
|
|
90
91
|
timestamp: Date.now(),
|
|
91
92
|
},
|
|
92
93
|
];
|
|
@@ -130,7 +131,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
130
131
|
|
|
131
132
|
state.history.push({
|
|
132
133
|
passage: passageName,
|
|
133
|
-
variables:
|
|
134
|
+
variables: deepClone(state.variables),
|
|
134
135
|
timestamp: Date.now(),
|
|
135
136
|
});
|
|
136
137
|
state.historyIndex = state.history.length - 1;
|
|
@@ -147,7 +148,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
147
148
|
state.historyIndex--;
|
|
148
149
|
const moment = state.history[state.historyIndex];
|
|
149
150
|
state.currentPassage = moment.passage;
|
|
150
|
-
state.variables =
|
|
151
|
+
state.variables = deepClone(moment.variables);
|
|
151
152
|
state.temporary = {};
|
|
152
153
|
});
|
|
153
154
|
},
|
|
@@ -158,7 +159,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
158
159
|
state.historyIndex++;
|
|
159
160
|
const moment = state.history[state.historyIndex];
|
|
160
161
|
state.currentPassage = moment.passage;
|
|
161
|
-
state.variables =
|
|
162
|
+
state.variables = deepClone(moment.variables);
|
|
162
163
|
state.temporary = {};
|
|
163
164
|
});
|
|
164
165
|
},
|
|
@@ -201,7 +202,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
201
202
|
const startPassage = storyData.passagesById.get(storyData.startNode);
|
|
202
203
|
if (!startPassage) return;
|
|
203
204
|
|
|
204
|
-
const initialVars =
|
|
205
|
+
const initialVars = deepClone(variableDefaults);
|
|
205
206
|
|
|
206
207
|
set((state) => {
|
|
207
208
|
state.currentPassage = startPassage.name;
|
|
@@ -210,7 +211,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
210
211
|
state.history = [
|
|
211
212
|
{
|
|
212
213
|
passage: startPassage.name,
|
|
213
|
-
variables:
|
|
214
|
+
variables: deepClone(initialVars),
|
|
214
215
|
timestamp: Date.now(),
|
|
215
216
|
},
|
|
216
217
|
];
|
|
@@ -244,8 +245,8 @@ export const useStoryStore = create<StoryState>()(
|
|
|
244
245
|
|
|
245
246
|
const payload: SavePayload = {
|
|
246
247
|
passage: currentPassage,
|
|
247
|
-
variables:
|
|
248
|
-
history:
|
|
248
|
+
variables: deepClone(variables),
|
|
249
|
+
history: deepClone(history),
|
|
249
250
|
historyIndex,
|
|
250
251
|
visitCounts: { ...visitCounts },
|
|
251
252
|
renderCounts: { ...renderCounts },
|
|
@@ -294,8 +295,8 @@ export const useStoryStore = create<StoryState>()(
|
|
|
294
295
|
} = get();
|
|
295
296
|
return {
|
|
296
297
|
passage: currentPassage,
|
|
297
|
-
variables:
|
|
298
|
-
history:
|
|
298
|
+
variables: deepClone(variables),
|
|
299
|
+
history: deepClone(history),
|
|
299
300
|
historyIndex,
|
|
300
301
|
visitCounts: { ...visitCounts },
|
|
301
302
|
renderCounts: { ...renderCounts },
|
|
@@ -305,8 +306,11 @@ export const useStoryStore = create<StoryState>()(
|
|
|
305
306
|
loadFromPayload: (payload: SavePayload) => {
|
|
306
307
|
set((state) => {
|
|
307
308
|
state.currentPassage = payload.passage;
|
|
308
|
-
state.variables = payload.variables;
|
|
309
|
-
state.history = payload.history
|
|
309
|
+
state.variables = deserialize(payload.variables);
|
|
310
|
+
state.history = payload.history.map((m) => ({
|
|
311
|
+
...m,
|
|
312
|
+
variables: deserialize(m.variables),
|
|
313
|
+
}));
|
|
310
314
|
state.historyIndex = payload.historyIndex;
|
|
311
315
|
state.visitCounts = payload.visitCounts ?? {};
|
|
312
316
|
state.renderCounts = payload.renderCounts ?? {};
|
package/src/story-api.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { useStoryStore } from './store';
|
|
|
2
2
|
import { settings } from './settings';
|
|
3
3
|
import type { SavePayload } from './saves/types';
|
|
4
4
|
import { setTitleGenerator } from './saves/save-manager';
|
|
5
|
+
import { registerClass } from './class-registry';
|
|
5
6
|
|
|
6
7
|
export interface StoryAPI {
|
|
7
8
|
get(name: string): unknown;
|
|
@@ -24,6 +25,7 @@ export interface StoryAPI {
|
|
|
24
25
|
hasRenderedAll(...names: string[]): boolean;
|
|
25
26
|
readonly title: string;
|
|
26
27
|
readonly settings: typeof settings;
|
|
28
|
+
registerClass(name: string, ctor: new (...args: any[]) => any): void;
|
|
27
29
|
readonly saves: {
|
|
28
30
|
setTitleGenerator(fn: (payload: SavePayload) => string): void;
|
|
29
31
|
};
|
|
@@ -116,6 +118,10 @@ function createStoryAPI(): StoryAPI {
|
|
|
116
118
|
|
|
117
119
|
settings,
|
|
118
120
|
|
|
121
|
+
registerClass(name: string, ctor: new (...args: any[]) => any): void {
|
|
122
|
+
registerClass(name, ctor);
|
|
123
|
+
},
|
|
124
|
+
|
|
119
125
|
saves: {
|
|
120
126
|
setTitleGenerator(fn: (payload: SavePayload) => string): void {
|
|
121
127
|
setTitleGenerator(fn);
|
package/src/story-init.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { buildAST } from './markup/ast';
|
|
|
4
4
|
import { execute } from './expression';
|
|
5
5
|
import type { ASTNode } from './markup/ast';
|
|
6
6
|
import { setSaveTitlePassage } from './saves/save-manager';
|
|
7
|
+
import { deepClone } from './class-registry';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Walk AST nodes from StoryInit and execute {set} and {do} imperatively
|
|
@@ -42,8 +43,8 @@ export function executeStoryInit() {
|
|
|
42
43
|
const tokens = tokenize(storyInit.content);
|
|
43
44
|
const ast = buildAST(tokens);
|
|
44
45
|
|
|
45
|
-
const vars =
|
|
46
|
-
const temps =
|
|
46
|
+
const vars = deepClone(state.variables);
|
|
47
|
+
const temps = deepClone(state.temporary);
|
|
47
48
|
|
|
48
49
|
walkAndExecute(ast, vars, temps);
|
|
49
50
|
|
package/src/styles.css
CHANGED
|
@@ -629,6 +629,36 @@ tw-storydata {
|
|
|
629
629
|
border-color: #90caf9;
|
|
630
630
|
}
|
|
631
631
|
|
|
632
|
+
/* Macro: meter */
|
|
633
|
+
|
|
634
|
+
.macro-meter {
|
|
635
|
+
position: relative;
|
|
636
|
+
width: 100%;
|
|
637
|
+
height: 1.4em;
|
|
638
|
+
background: #2a2a3e;
|
|
639
|
+
border-radius: 0.3em;
|
|
640
|
+
overflow: hidden;
|
|
641
|
+
border: 1px solid #444;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.macro-meter-fill {
|
|
645
|
+
height: 100%;
|
|
646
|
+
background: #64b5f6;
|
|
647
|
+
transition: width 0.3s ease;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.macro-meter-label {
|
|
651
|
+
position: absolute;
|
|
652
|
+
inset: 0;
|
|
653
|
+
display: flex;
|
|
654
|
+
align-items: center;
|
|
655
|
+
justify-content: center;
|
|
656
|
+
font-size: 0.85em;
|
|
657
|
+
color: #e0e0e0;
|
|
658
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
|
|
659
|
+
pointer-events: none;
|
|
660
|
+
}
|
|
661
|
+
|
|
632
662
|
/* Markdown wrapper — invisible to layout */
|
|
633
663
|
|
|
634
664
|
.md {
|
|
@@ -777,4 +807,4 @@ tw-storydata {
|
|
|
777
807
|
50% {
|
|
778
808
|
opacity: 0;
|
|
779
809
|
}
|
|
780
|
-
}
|
|
810
|
+
}
|