squiffy-runtime 6.0.0-alpha.1 → 6.0.0-alpha.11
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/events.d.ts +15 -0
- package/dist/events.js +35 -0
- package/dist/squiffy.runtime.d.ts +2 -0
- package/dist/squiffy.runtime.js +529 -0
- package/dist/squiffy.runtime.test.d.ts +1 -0
- package/dist/squiffy.runtime.test.js +320 -0
- package/dist/textProcessor.d.ts +15 -0
- package/dist/textProcessor.js +165 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +14 -0
- package/package.json +20 -6
- package/src/__snapshots__/squiffy.runtime.test.ts.snap +104 -0
- package/src/events.ts +42 -0
- package/src/squiffy.runtime.test.ts +413 -0
- package/src/squiffy.runtime.ts +203 -304
- package/src/textProcessor.ts +177 -0
- package/src/types.ts +78 -0
- package/src/utils.ts +14 -0
- package/tsconfig.json +5 -1
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { expect, test, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import globalJsdom from 'global-jsdom';
|
|
4
|
+
import { init } from './squiffy.runtime.js';
|
|
5
|
+
import { compile as squiffyCompile } from 'squiffy-compiler';
|
|
6
|
+
const html = `
|
|
7
|
+
<!DOCTYPE html>
|
|
8
|
+
<html>
|
|
9
|
+
<head>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="squiffy">
|
|
13
|
+
</div>
|
|
14
|
+
<div id="test">
|
|
15
|
+
</div>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
18
|
+
`;
|
|
19
|
+
const compile = async (script) => {
|
|
20
|
+
const compileResult = await squiffyCompile({
|
|
21
|
+
script: script,
|
|
22
|
+
});
|
|
23
|
+
if (!compileResult.success) {
|
|
24
|
+
throw new Error('Compile failed');
|
|
25
|
+
}
|
|
26
|
+
const story = compileResult.output.story;
|
|
27
|
+
const js = compileResult.output.js.map(jsLines => new Function('squiffy', 'get', 'set', jsLines.join('\n')));
|
|
28
|
+
return {
|
|
29
|
+
story: {
|
|
30
|
+
js: js,
|
|
31
|
+
...story,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
const initScript = async (script) => {
|
|
36
|
+
const element = document.getElementById('squiffy');
|
|
37
|
+
if (!element) {
|
|
38
|
+
throw new Error('Element not found');
|
|
39
|
+
}
|
|
40
|
+
const compileResult = await compile(script);
|
|
41
|
+
const squiffyApi = init({
|
|
42
|
+
element: element,
|
|
43
|
+
story: compileResult.story,
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
squiffyApi,
|
|
47
|
+
element
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
const findLink = (element, linkType, linkText, onlyEnabled = false) => {
|
|
51
|
+
const links = onlyEnabled
|
|
52
|
+
? element.querySelectorAll(`.squiffy-output-section:last-child a.squiffy-link.link-${linkType}`)
|
|
53
|
+
: element.querySelectorAll(`a.squiffy-link.link-${linkType}`);
|
|
54
|
+
return Array.from(links).find(link => link.textContent === linkText && (onlyEnabled ? !link.classList.contains("disabled") : true));
|
|
55
|
+
};
|
|
56
|
+
const getTestOutput = () => {
|
|
57
|
+
const testElement = document.getElementById('test');
|
|
58
|
+
if (!testElement) {
|
|
59
|
+
throw new Error('Test element not found');
|
|
60
|
+
}
|
|
61
|
+
return testElement.innerText;
|
|
62
|
+
};
|
|
63
|
+
let cleanup;
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
cleanup = globalJsdom(html);
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
cleanup();
|
|
69
|
+
});
|
|
70
|
+
test('"Hello world" script should run', async () => {
|
|
71
|
+
const { element } = await initScript("Hello world");
|
|
72
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
73
|
+
});
|
|
74
|
+
test('Click a section link', async () => {
|
|
75
|
+
const script = await fs.readFile('../examples/test/example.squiffy', 'utf-8');
|
|
76
|
+
const { squiffyApi, element } = await initScript(script);
|
|
77
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
78
|
+
expect(element.querySelectorAll('a.squiffy-link').length).toBe(10);
|
|
79
|
+
const linkToPassage = findLink(element, 'passage', 'a link to a passage');
|
|
80
|
+
const section3Link = findLink(element, 'section', 'section 3');
|
|
81
|
+
expect(linkToPassage).not.toBeNull();
|
|
82
|
+
expect(section3Link).not.toBeNull();
|
|
83
|
+
expect(linkToPassage.classList).not.toContain('disabled');
|
|
84
|
+
const handler = vi.fn();
|
|
85
|
+
const off = squiffyApi.on('linkClick', handler);
|
|
86
|
+
const handled = squiffyApi.clickLink(section3Link);
|
|
87
|
+
expect(handled).toBe(true);
|
|
88
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
89
|
+
// passage link is from the previous section, so should be unclickable
|
|
90
|
+
expect(squiffyApi.clickLink(linkToPassage)).toBe(false);
|
|
91
|
+
// await for the event to be processed
|
|
92
|
+
await Promise.resolve();
|
|
93
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
94
|
+
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ linkType: 'section' }));
|
|
95
|
+
off();
|
|
96
|
+
});
|
|
97
|
+
test('Click a passage link', async () => {
|
|
98
|
+
const script = await fs.readFile('../examples/test/example.squiffy', 'utf-8');
|
|
99
|
+
const { squiffyApi, element } = await initScript(script);
|
|
100
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
101
|
+
expect(element.querySelectorAll('a.squiffy-link').length).toBe(10);
|
|
102
|
+
const linkToPassage = findLink(element, 'passage', 'a link to a passage');
|
|
103
|
+
const section3Link = findLink(element, 'section', 'section 3');
|
|
104
|
+
expect(linkToPassage).not.toBeNull();
|
|
105
|
+
expect(section3Link).not.toBeNull();
|
|
106
|
+
expect(linkToPassage.classList).not.toContain('disabled');
|
|
107
|
+
const handler = vi.fn();
|
|
108
|
+
const off = squiffyApi.on('linkClick', handler);
|
|
109
|
+
const handled = squiffyApi.clickLink(linkToPassage);
|
|
110
|
+
expect(handled).toBe(true);
|
|
111
|
+
expect(linkToPassage.classList).toContain('disabled');
|
|
112
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
113
|
+
// shouldn't be able to click it again
|
|
114
|
+
expect(squiffyApi.clickLink(linkToPassage)).toBe(false);
|
|
115
|
+
// await for the event to be processed
|
|
116
|
+
await Promise.resolve();
|
|
117
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
118
|
+
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ linkType: 'passage' }));
|
|
119
|
+
off();
|
|
120
|
+
});
|
|
121
|
+
test('Run JavaScript functions', async () => {
|
|
122
|
+
const script = `
|
|
123
|
+
document.getElementById('test').innerText = 'Initial JavaScript';
|
|
124
|
+
@set some_string = some_value
|
|
125
|
+
@set some_number = 5
|
|
126
|
+
|
|
127
|
+
+++Continue...
|
|
128
|
+
document.getElementById('test').innerText = "Value: " + get("some_number");
|
|
129
|
+
+++Continue...
|
|
130
|
+
document.getElementById('test').innerText = "Value: " + get("some_string");
|
|
131
|
+
set("some_number", 10);
|
|
132
|
+
+++Continue...
|
|
133
|
+
document.getElementById('test').innerText = "Value: " + get("some_number");
|
|
134
|
+
+++Continue...
|
|
135
|
+
@inc some_number
|
|
136
|
+
+++Continue...
|
|
137
|
+
document.getElementById('test').innerText = "Value: " + get("some_number");
|
|
138
|
+
+++Continue...
|
|
139
|
+
squiffy.story.go("other section");
|
|
140
|
+
[[other section]]:
|
|
141
|
+
document.getElementById('test').innerText = "In other section";
|
|
142
|
+
`;
|
|
143
|
+
const clickContinue = () => {
|
|
144
|
+
const continueLink = findLink(element, 'section', 'Continue...', true);
|
|
145
|
+
expect(continueLink).not.toBeNull();
|
|
146
|
+
const handled = squiffyApi.clickLink(continueLink);
|
|
147
|
+
expect(handled).toBe(true);
|
|
148
|
+
};
|
|
149
|
+
const { squiffyApi, element } = await initScript(script);
|
|
150
|
+
expect(getTestOutput()).toBe('Initial JavaScript');
|
|
151
|
+
clickContinue();
|
|
152
|
+
expect(getTestOutput()).toBe('Value: 5');
|
|
153
|
+
clickContinue();
|
|
154
|
+
expect(getTestOutput()).toBe('Value: some_value');
|
|
155
|
+
clickContinue();
|
|
156
|
+
expect(getTestOutput()).toBe('Value: 10');
|
|
157
|
+
clickContinue();
|
|
158
|
+
clickContinue();
|
|
159
|
+
expect(getTestOutput()).toBe('Value: 11');
|
|
160
|
+
clickContinue();
|
|
161
|
+
expect(getTestOutput()).toBe('In other section');
|
|
162
|
+
});
|
|
163
|
+
function safeQuerySelector(name) {
|
|
164
|
+
return name.replace(/'/g, "\\'");
|
|
165
|
+
}
|
|
166
|
+
function getSectionContent(element, section) {
|
|
167
|
+
return element.querySelector(`[data-source='[[${safeQuerySelector(section)}]]'] p`)?.textContent || null;
|
|
168
|
+
}
|
|
169
|
+
function getPassageContent(element, section, passage) {
|
|
170
|
+
return element.querySelector(`[data-source='[[${safeQuerySelector(section)}]][${safeQuerySelector(passage)}]'] p`)?.textContent || null;
|
|
171
|
+
}
|
|
172
|
+
test('Update default section output', async () => {
|
|
173
|
+
const { squiffyApi, element } = await initScript("Hello world");
|
|
174
|
+
let defaultOutput = getSectionContent(element, '_default');
|
|
175
|
+
expect(defaultOutput).toBe('Hello world');
|
|
176
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
177
|
+
const updated = await compile("Updated content");
|
|
178
|
+
squiffyApi.update(updated.story);
|
|
179
|
+
defaultOutput = getSectionContent(element, '_default');
|
|
180
|
+
expect(defaultOutput).toBe('Updated content');
|
|
181
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
182
|
+
});
|
|
183
|
+
test.each(['a', 'a\'1'])('Update passage output - passage name "%s"', async (name) => {
|
|
184
|
+
const { squiffyApi, element } = await initScript(`Click this: [${name}]
|
|
185
|
+
|
|
186
|
+
[${name}]:
|
|
187
|
+
Passage a content`);
|
|
188
|
+
const link = findLink(element, 'passage', name);
|
|
189
|
+
const handled = squiffyApi.clickLink(link);
|
|
190
|
+
expect(handled).toBe(true);
|
|
191
|
+
let defaultOutput = getSectionContent(element, '_default');
|
|
192
|
+
expect(defaultOutput).toBe(`Click this: ${name}`);
|
|
193
|
+
let passageOutput = getPassageContent(element, '_default', name);
|
|
194
|
+
expect(passageOutput).toBe('Passage a content');
|
|
195
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
196
|
+
const updated = await compile(`Click this: [${name}]
|
|
197
|
+
|
|
198
|
+
[${name}]:
|
|
199
|
+
Updated passage content`);
|
|
200
|
+
squiffyApi.update(updated.story);
|
|
201
|
+
defaultOutput = getSectionContent(element, '_default');
|
|
202
|
+
expect(defaultOutput).toBe(`Click this: ${name}`);
|
|
203
|
+
passageOutput = getPassageContent(element, '_default', name);
|
|
204
|
+
expect(passageOutput).toBe('Updated passage content');
|
|
205
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
206
|
+
});
|
|
207
|
+
test('Delete section', async () => {
|
|
208
|
+
const { squiffyApi, element } = await initScript(`Click this: [[a]]
|
|
209
|
+
|
|
210
|
+
[[a]]:
|
|
211
|
+
New section`);
|
|
212
|
+
const link = findLink(element, 'section', 'a');
|
|
213
|
+
const handled = squiffyApi.clickLink(link);
|
|
214
|
+
expect(handled).toBe(true);
|
|
215
|
+
let defaultOutput = getSectionContent(element, '_default');
|
|
216
|
+
expect(defaultOutput).toBe('Click this: a');
|
|
217
|
+
let sectionOutput = getSectionContent(element, 'a');
|
|
218
|
+
expect(sectionOutput).toBe('New section');
|
|
219
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
220
|
+
const updated = await compile(`Click this: [[a]]`);
|
|
221
|
+
squiffyApi.update(updated.story);
|
|
222
|
+
defaultOutput = getSectionContent(element, '_default');
|
|
223
|
+
expect(defaultOutput).toBe('Click this: a');
|
|
224
|
+
sectionOutput = getSectionContent(element, 'a');
|
|
225
|
+
expect(sectionOutput).toBeNull();
|
|
226
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
227
|
+
});
|
|
228
|
+
test('Delete passage', async () => {
|
|
229
|
+
const { squiffyApi, element } = await initScript(`Click this: [a]
|
|
230
|
+
|
|
231
|
+
[a]:
|
|
232
|
+
New passage`);
|
|
233
|
+
const link = findLink(element, 'passage', 'a');
|
|
234
|
+
const handled = squiffyApi.clickLink(link);
|
|
235
|
+
expect(handled).toBe(true);
|
|
236
|
+
let defaultOutput = getSectionContent(element, '_default');
|
|
237
|
+
expect(defaultOutput).toBe('Click this: a');
|
|
238
|
+
let passageOutput = getPassageContent(element, '_default', 'a');
|
|
239
|
+
expect(passageOutput).toBe('New passage');
|
|
240
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
241
|
+
const updated = await compile(`Click this: [a]`);
|
|
242
|
+
squiffyApi.update(updated.story);
|
|
243
|
+
defaultOutput = getSectionContent(element, '_default');
|
|
244
|
+
expect(defaultOutput).toBe('Click this: a');
|
|
245
|
+
passageOutput = getPassageContent(element, '_default', 'a');
|
|
246
|
+
expect(passageOutput).toBeNull();
|
|
247
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
248
|
+
});
|
|
249
|
+
test('Clicked passage links remain disabled after an update', async () => {
|
|
250
|
+
const { squiffyApi, element } = await initScript(`Click one of these: [a] [b]
|
|
251
|
+
|
|
252
|
+
[a]:
|
|
253
|
+
Output for passage A.
|
|
254
|
+
|
|
255
|
+
[b]:
|
|
256
|
+
Output for passage B.`);
|
|
257
|
+
// click linkA
|
|
258
|
+
let linkA = findLink(element, 'passage', 'a');
|
|
259
|
+
expect(linkA.classList).not.toContain('disabled');
|
|
260
|
+
expect(squiffyApi.clickLink(linkA)).toBe(true);
|
|
261
|
+
const updated = await compile(`Click one of these (updated): [a] [b]
|
|
262
|
+
|
|
263
|
+
[a]:
|
|
264
|
+
Output for passage A.
|
|
265
|
+
|
|
266
|
+
[b]:
|
|
267
|
+
Output for passage B.`);
|
|
268
|
+
squiffyApi.update(updated.story);
|
|
269
|
+
// linkA should still be disabled
|
|
270
|
+
linkA = findLink(element, 'passage', 'a');
|
|
271
|
+
expect(linkA.classList).toContain('disabled');
|
|
272
|
+
expect(squiffyApi.clickLink(linkA)).toBe(false);
|
|
273
|
+
// linkB should still be enabled
|
|
274
|
+
let linkB = findLink(element, 'passage', 'b');
|
|
275
|
+
expect(linkB.classList).not.toContain('disabled');
|
|
276
|
+
expect(squiffyApi.clickLink(linkB)).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
test('Deleting the current section activates the previous section', async () => {
|
|
279
|
+
const { squiffyApi, element } = await initScript(`Choose a section: [[a]] [[b]], or passage [start1].
|
|
280
|
+
|
|
281
|
+
[start1]:
|
|
282
|
+
Output for passage start1.
|
|
283
|
+
|
|
284
|
+
[[a]]:
|
|
285
|
+
Output for section A.
|
|
286
|
+
|
|
287
|
+
[[b]]:
|
|
288
|
+
Output for section B.`);
|
|
289
|
+
// click linkA
|
|
290
|
+
let linkA = findLink(element, 'section', 'a');
|
|
291
|
+
let linkB = findLink(element, 'section', 'b');
|
|
292
|
+
expect(linkA.classList).not.toContain('disabled');
|
|
293
|
+
expect(squiffyApi.clickLink(linkA)).toBe(true);
|
|
294
|
+
// can't click start1 passage as we're in section [[a]] now
|
|
295
|
+
let linkStart1 = findLink(element, 'passage', 'start1');
|
|
296
|
+
expect(squiffyApi.clickLink(linkStart1)).toBe(false);
|
|
297
|
+
// can't click linkB as we're in section [[a]] now
|
|
298
|
+
expect(squiffyApi.clickLink(linkB)).toBe(false);
|
|
299
|
+
// now we delete section [[a]]
|
|
300
|
+
const updated = await compile(`Choose a section: [[a]] [[b]], or passage [start1].
|
|
301
|
+
|
|
302
|
+
[start1]:
|
|
303
|
+
Output for passage start1.
|
|
304
|
+
|
|
305
|
+
[[b]]:
|
|
306
|
+
Output for section B. Here's a passage: [b1].
|
|
307
|
+
|
|
308
|
+
[b1]:
|
|
309
|
+
Passage in section B.`);
|
|
310
|
+
squiffyApi.update(updated.story);
|
|
311
|
+
// We're in the first section, so the start1 passage should be clickable now
|
|
312
|
+
linkStart1 = findLink(element, 'passage', 'start1');
|
|
313
|
+
expect(squiffyApi.clickLink(linkStart1)).toBe(true);
|
|
314
|
+
// We're in the first section, so linkB should be clickable now
|
|
315
|
+
linkB = findLink(element, 'section', 'b');
|
|
316
|
+
expect(squiffyApi.clickLink(linkB)).toBe(true);
|
|
317
|
+
// and the passage [b1] within it should be clickable
|
|
318
|
+
const linkB1 = findLink(element, 'passage', 'b1');
|
|
319
|
+
expect(squiffyApi.clickLink(linkB1)).toBe(true);
|
|
320
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class TextProcessor {
|
|
2
|
+
get: (attribute: string) => any;
|
|
3
|
+
set: (attribute: string, value: any) => void;
|
|
4
|
+
story: any;
|
|
5
|
+
currentSection: any;
|
|
6
|
+
seen: (section: string) => boolean;
|
|
7
|
+
processAttributes: (attributes: string[]) => void;
|
|
8
|
+
constructor(get: (attribute: string) => any, set: (attribute: string, value: any) => void, story: any, currentSection: any, seen: (section: string) => boolean, processAttributes: (attributes: string[]) => void);
|
|
9
|
+
process(text: string, data: any): string;
|
|
10
|
+
processTextCommand(text: string, data: any): any;
|
|
11
|
+
processTextCommand_If(section: string, data: any): string;
|
|
12
|
+
processTextCommand_Else(section: string, data: any): string;
|
|
13
|
+
processTextCommand_Label(section: string, data: any): string;
|
|
14
|
+
processTextCommand_Rotate(type: string, section: string): string;
|
|
15
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { startsWith, rotate } from "./utils.js";
|
|
2
|
+
export class TextProcessor {
|
|
3
|
+
constructor(get, set, story, currentSection, seen, processAttributes) {
|
|
4
|
+
this.get = get;
|
|
5
|
+
this.set = set;
|
|
6
|
+
this.story = story;
|
|
7
|
+
this.currentSection = currentSection;
|
|
8
|
+
this.seen = seen;
|
|
9
|
+
this.processAttributes = processAttributes;
|
|
10
|
+
}
|
|
11
|
+
process(text, data) {
|
|
12
|
+
let containsUnprocessedSection = false;
|
|
13
|
+
const open = text.indexOf('{');
|
|
14
|
+
let close;
|
|
15
|
+
if (open > -1) {
|
|
16
|
+
let nestCount = 1;
|
|
17
|
+
let searchStart = open + 1;
|
|
18
|
+
let finished = false;
|
|
19
|
+
while (!finished) {
|
|
20
|
+
const nextOpen = text.indexOf('{', searchStart);
|
|
21
|
+
const nextClose = text.indexOf('}', searchStart);
|
|
22
|
+
if (nextClose > -1) {
|
|
23
|
+
if (nextOpen > -1 && nextOpen < nextClose) {
|
|
24
|
+
nestCount++;
|
|
25
|
+
searchStart = nextOpen + 1;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
nestCount--;
|
|
29
|
+
searchStart = nextClose + 1;
|
|
30
|
+
if (nestCount === 0) {
|
|
31
|
+
close = nextClose;
|
|
32
|
+
containsUnprocessedSection = true;
|
|
33
|
+
finished = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
finished = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (containsUnprocessedSection) {
|
|
43
|
+
const section = text.substring(open + 1, close);
|
|
44
|
+
const value = this.processTextCommand(section, data);
|
|
45
|
+
text = text.substring(0, open) + value + this.process(text.substring(close + 1), data);
|
|
46
|
+
}
|
|
47
|
+
return (text);
|
|
48
|
+
}
|
|
49
|
+
processTextCommand(text, data) {
|
|
50
|
+
if (startsWith(text, 'if ')) {
|
|
51
|
+
return this.processTextCommand_If(text, data);
|
|
52
|
+
}
|
|
53
|
+
else if (startsWith(text, 'else:')) {
|
|
54
|
+
return this.processTextCommand_Else(text, data);
|
|
55
|
+
}
|
|
56
|
+
else if (startsWith(text, 'label:')) {
|
|
57
|
+
return this.processTextCommand_Label(text, data);
|
|
58
|
+
}
|
|
59
|
+
else if (/^rotate[: ]/.test(text)) {
|
|
60
|
+
return this.processTextCommand_Rotate('rotate', text);
|
|
61
|
+
}
|
|
62
|
+
else if (/^sequence[: ]/.test(text)) {
|
|
63
|
+
return this.processTextCommand_Rotate('sequence', text);
|
|
64
|
+
}
|
|
65
|
+
else if (this.currentSection.passages && text in this.currentSection.passages) {
|
|
66
|
+
return this.process(this.currentSection.passages[text].text || '', data);
|
|
67
|
+
}
|
|
68
|
+
else if (text in this.story.sections) {
|
|
69
|
+
return this.process(this.story.sections[text].text || '', data);
|
|
70
|
+
}
|
|
71
|
+
else if (startsWith(text, '@') && !startsWith(text, '@replace')) {
|
|
72
|
+
this.processAttributes(text.substring(1).split(","));
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
return this.get(text);
|
|
76
|
+
}
|
|
77
|
+
processTextCommand_If(section, data) {
|
|
78
|
+
const command = section.substring(3);
|
|
79
|
+
const colon = command.indexOf(':');
|
|
80
|
+
if (colon == -1) {
|
|
81
|
+
return ('{if ' + command + '}');
|
|
82
|
+
}
|
|
83
|
+
const text = command.substring(colon + 1);
|
|
84
|
+
let condition = command.substring(0, colon);
|
|
85
|
+
condition = condition.replace("<", "<");
|
|
86
|
+
const operatorRegex = /([\w ]*)(=|<=|>=|<>|<|>)(.*)/;
|
|
87
|
+
const match = operatorRegex.exec(condition);
|
|
88
|
+
let result = false;
|
|
89
|
+
if (match) {
|
|
90
|
+
const lhs = this.get(match[1]);
|
|
91
|
+
const op = match[2];
|
|
92
|
+
let rhs = match[3];
|
|
93
|
+
if (startsWith(rhs, '@'))
|
|
94
|
+
rhs = this.get(rhs.substring(1));
|
|
95
|
+
if (op == '=' && lhs == rhs)
|
|
96
|
+
result = true;
|
|
97
|
+
if (op == '<>' && lhs != rhs)
|
|
98
|
+
result = true;
|
|
99
|
+
if (op == '>' && lhs > rhs)
|
|
100
|
+
result = true;
|
|
101
|
+
if (op == '<' && lhs < rhs)
|
|
102
|
+
result = true;
|
|
103
|
+
if (op == '>=' && lhs >= rhs)
|
|
104
|
+
result = true;
|
|
105
|
+
if (op == '<=' && lhs <= rhs)
|
|
106
|
+
result = true;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
let checkValue = true;
|
|
110
|
+
if (startsWith(condition, 'not ')) {
|
|
111
|
+
condition = condition.substring(4);
|
|
112
|
+
checkValue = false;
|
|
113
|
+
}
|
|
114
|
+
if (startsWith(condition, 'seen ')) {
|
|
115
|
+
result = (this.seen(condition.substring(5)) == checkValue);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
let value = this.get(condition);
|
|
119
|
+
if (value === null)
|
|
120
|
+
value = false;
|
|
121
|
+
result = (value == checkValue);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const textResult = result ? this.process(text, data) : '';
|
|
125
|
+
data.lastIf = result;
|
|
126
|
+
return textResult;
|
|
127
|
+
}
|
|
128
|
+
processTextCommand_Else(section, data) {
|
|
129
|
+
if (!('lastIf' in data) || data.lastIf)
|
|
130
|
+
return '';
|
|
131
|
+
const text = section.substring(5);
|
|
132
|
+
return this.process(text, data);
|
|
133
|
+
}
|
|
134
|
+
processTextCommand_Label(section, data) {
|
|
135
|
+
const command = section.substring(6);
|
|
136
|
+
const eq = command.indexOf('=');
|
|
137
|
+
if (eq == -1) {
|
|
138
|
+
return ('{label:' + command + '}');
|
|
139
|
+
}
|
|
140
|
+
const text = command.substring(eq + 1);
|
|
141
|
+
const label = command.substring(0, eq);
|
|
142
|
+
return '<span class="squiffy-label-' + label + '">' + this.process(text, data) + '</span>';
|
|
143
|
+
}
|
|
144
|
+
processTextCommand_Rotate(type, section) {
|
|
145
|
+
let options;
|
|
146
|
+
let attribute = '';
|
|
147
|
+
if (section.substring(type.length, type.length + 1) == ' ') {
|
|
148
|
+
const colon = section.indexOf(':');
|
|
149
|
+
if (colon == -1) {
|
|
150
|
+
return '{' + section + '}';
|
|
151
|
+
}
|
|
152
|
+
options = section.substring(colon + 1);
|
|
153
|
+
attribute = section.substring(type.length + 1, colon);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
options = section.substring(type.length + 1);
|
|
157
|
+
}
|
|
158
|
+
// TODO: Check - previously there was no second parameter here
|
|
159
|
+
const rotation = rotate(options.replace(/"/g, '"').replace(/'/g, '''), null);
|
|
160
|
+
if (attribute) {
|
|
161
|
+
this.set(attribute, rotation[0]);
|
|
162
|
+
}
|
|
163
|
+
return '<a class="squiffy-link" data-' + type + '="' + rotation[1] + '" data-attribute="' + attribute + '" role="link">' + rotation[0] + '</a>';
|
|
164
|
+
}
|
|
165
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { SquiffyEventHandler, SquiffyEventMap } from "./events.js";
|
|
2
|
+
export interface SquiffyInitOptions {
|
|
3
|
+
element: HTMLElement;
|
|
4
|
+
story: Story;
|
|
5
|
+
scroll?: string;
|
|
6
|
+
persist?: boolean;
|
|
7
|
+
onSet?: (attribute: string, value: any) => void;
|
|
8
|
+
}
|
|
9
|
+
export interface SquiffySettings {
|
|
10
|
+
scroll: string;
|
|
11
|
+
persist: boolean;
|
|
12
|
+
onSet: (attribute: string, value: any) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface SquiffyApi {
|
|
15
|
+
restart: () => void;
|
|
16
|
+
get: (attribute: string) => any;
|
|
17
|
+
set: (attribute: string, value: any) => void;
|
|
18
|
+
clickLink: (link: HTMLElement) => boolean;
|
|
19
|
+
update: (story: Story) => void;
|
|
20
|
+
on<E extends keyof SquiffyEventMap>(event: E, handler: SquiffyEventHandler<E>): () => void;
|
|
21
|
+
off<E extends keyof SquiffyEventMap>(event: E, handler: SquiffyEventHandler<E>): void;
|
|
22
|
+
once<E extends keyof SquiffyEventMap>(event: E, handler: SquiffyEventHandler<E>): () => void;
|
|
23
|
+
}
|
|
24
|
+
interface SquiffyJsFunctionApi {
|
|
25
|
+
get: (attribute: string) => any;
|
|
26
|
+
set: (attribute: string, value: any) => void;
|
|
27
|
+
ui: {
|
|
28
|
+
transition: (f: any) => void;
|
|
29
|
+
};
|
|
30
|
+
story: {
|
|
31
|
+
go: (section: string) => void;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface Story {
|
|
35
|
+
js: ((squiffy: SquiffyJsFunctionApi, get: (attribute: string) => any, set: (attribute: string, value: any) => void) => void)[];
|
|
36
|
+
start: string;
|
|
37
|
+
id?: string | null;
|
|
38
|
+
sections: Record<string, Section>;
|
|
39
|
+
}
|
|
40
|
+
export interface Section {
|
|
41
|
+
text?: string;
|
|
42
|
+
clear?: boolean;
|
|
43
|
+
attributes?: string[];
|
|
44
|
+
jsIndex?: number;
|
|
45
|
+
passages?: Record<string, Passage>;
|
|
46
|
+
passageCount?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface Passage {
|
|
49
|
+
text?: string;
|
|
50
|
+
clear?: boolean;
|
|
51
|
+
attributes?: string[];
|
|
52
|
+
jsIndex?: number;
|
|
53
|
+
}
|
|
54
|
+
export {};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function startsWith(string, prefix) {
|
|
2
|
+
return string.substring(0, prefix.length) === prefix;
|
|
3
|
+
}
|
|
4
|
+
export function rotate(options, current) {
|
|
5
|
+
const colon = options.indexOf(':');
|
|
6
|
+
if (colon == -1) {
|
|
7
|
+
return [options, current];
|
|
8
|
+
}
|
|
9
|
+
const next = options.substring(0, colon);
|
|
10
|
+
let remaining = options.substring(colon + 1);
|
|
11
|
+
if (current)
|
|
12
|
+
remaining += ':' + current;
|
|
13
|
+
return [next, remaining];
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squiffy-runtime",
|
|
3
|
-
"version": "6.0.0-alpha.
|
|
4
|
-
"
|
|
3
|
+
"version": "6.0.0-alpha.11",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/squiffy.runtime.js",
|
|
6
|
+
"types": "dist/squiffy.runtime.d.ts",
|
|
5
7
|
"scripts": {
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
+
"dev": "vitest",
|
|
9
|
+
"test": "vitest --run",
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"prepublishOnly": "npm run build && npm run test"
|
|
8
12
|
},
|
|
9
13
|
"author": "Alex Warren",
|
|
14
|
+
"contributors": [
|
|
15
|
+
"CrisisSDK",
|
|
16
|
+
"mrangel",
|
|
17
|
+
"Luis Felipe Morales"
|
|
18
|
+
],
|
|
10
19
|
"license": "MIT",
|
|
11
20
|
"description": "",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/jsdom": "^21.1.7",
|
|
23
|
+
"global-jsdom": "^25.0.0",
|
|
24
|
+
"jsdom": "^25.0.0",
|
|
25
|
+
"squiffy-compiler": "^6.0.0-alpha.3",
|
|
26
|
+
"typescript": "^5.6.2",
|
|
27
|
+
"vitest": "^2.1.1"
|
|
14
28
|
}
|
|
15
29
|
}
|