squiffy-runtime 6.0.0-alpha.1 → 6.0.0-alpha.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/squiffy.runtime.d.ts +45 -0
- package/dist/squiffy.runtime.js +603 -0
- package/package.json +16 -5
- package/src/__snapshots__/squiffy.runtime.test.ts.snap +54 -0
- package/src/squiffy.runtime.test.ts +173 -0
- package/src/squiffy.runtime.ts +82 -56
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
interface SquiffyInitOptions {
|
|
2
|
+
element: HTMLElement;
|
|
3
|
+
story: Story;
|
|
4
|
+
scroll?: string;
|
|
5
|
+
persist?: boolean;
|
|
6
|
+
onSet?: (attribute: string, value: any) => void;
|
|
7
|
+
}
|
|
8
|
+
interface SquiffyApi {
|
|
9
|
+
restart: () => void;
|
|
10
|
+
get: (attribute: string) => any;
|
|
11
|
+
set: (attribute: string, value: any) => void;
|
|
12
|
+
clickLink: (link: HTMLElement) => void;
|
|
13
|
+
}
|
|
14
|
+
interface SquiffyJsFunctionApi {
|
|
15
|
+
get: (attribute: string) => any;
|
|
16
|
+
set: (attribute: string, value: any) => void;
|
|
17
|
+
ui: {
|
|
18
|
+
transition: (f: any) => void;
|
|
19
|
+
};
|
|
20
|
+
story: {
|
|
21
|
+
go: (section: string) => void;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
interface Story {
|
|
25
|
+
js: ((squiffy: SquiffyJsFunctionApi, get: (attribute: string) => any, set: (attribute: string, value: any) => void) => void)[];
|
|
26
|
+
start: string;
|
|
27
|
+
id?: string | null;
|
|
28
|
+
sections: Record<string, Section>;
|
|
29
|
+
}
|
|
30
|
+
interface Section {
|
|
31
|
+
text?: string;
|
|
32
|
+
clear?: boolean;
|
|
33
|
+
attributes?: string[];
|
|
34
|
+
jsIndex?: number;
|
|
35
|
+
passages?: Record<string, Passage>;
|
|
36
|
+
passageCount?: number;
|
|
37
|
+
}
|
|
38
|
+
interface Passage {
|
|
39
|
+
text?: string;
|
|
40
|
+
clear?: boolean;
|
|
41
|
+
attributes?: string[];
|
|
42
|
+
jsIndex?: number;
|
|
43
|
+
}
|
|
44
|
+
export declare const init: (options: SquiffyInitOptions) => SquiffyApi;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
export const init = (options) => {
|
|
2
|
+
let story;
|
|
3
|
+
let currentSection;
|
|
4
|
+
let currentSectionElement;
|
|
5
|
+
let scrollPosition = 0;
|
|
6
|
+
let outputElement;
|
|
7
|
+
let settings;
|
|
8
|
+
let storageFallback = {};
|
|
9
|
+
function set(attribute, value) {
|
|
10
|
+
if (typeof value === 'undefined')
|
|
11
|
+
value = true;
|
|
12
|
+
if (settings.persist && window.localStorage) {
|
|
13
|
+
localStorage[story.id + '-' + attribute] = JSON.stringify(value);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
storageFallback[attribute] = JSON.stringify(value);
|
|
17
|
+
}
|
|
18
|
+
settings.onSet(attribute, value);
|
|
19
|
+
}
|
|
20
|
+
function get(attribute) {
|
|
21
|
+
let result;
|
|
22
|
+
if (settings.persist && window.localStorage) {
|
|
23
|
+
result = localStorage[story.id + '-' + attribute];
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
result = storageFallback[attribute];
|
|
27
|
+
}
|
|
28
|
+
if (!result)
|
|
29
|
+
return null;
|
|
30
|
+
return JSON.parse(result);
|
|
31
|
+
}
|
|
32
|
+
function handleLink(link) {
|
|
33
|
+
if (link.classList.contains('disabled'))
|
|
34
|
+
return;
|
|
35
|
+
let passage = link.getAttribute('data-passage');
|
|
36
|
+
let section = link.getAttribute('data-section');
|
|
37
|
+
const rotateAttr = link.getAttribute('data-rotate');
|
|
38
|
+
const sequenceAttr = link.getAttribute('data-sequence');
|
|
39
|
+
const rotateOrSequenceAttr = rotateAttr || sequenceAttr;
|
|
40
|
+
if (passage) {
|
|
41
|
+
disableLink(link);
|
|
42
|
+
set('_turncount', get('_turncount') + 1);
|
|
43
|
+
passage = processLink(passage);
|
|
44
|
+
if (passage) {
|
|
45
|
+
currentSectionElement?.appendChild(document.createElement('hr'));
|
|
46
|
+
showPassage(passage);
|
|
47
|
+
}
|
|
48
|
+
const turnPassage = '@' + get('_turncount');
|
|
49
|
+
if (currentSection.passages) {
|
|
50
|
+
if (turnPassage in currentSection.passages) {
|
|
51
|
+
showPassage(turnPassage);
|
|
52
|
+
}
|
|
53
|
+
if ('@last' in currentSection.passages && get('_turncount') >= (currentSection.passageCount || 0)) {
|
|
54
|
+
showPassage('@last');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (section) {
|
|
59
|
+
currentSectionElement?.appendChild(document.createElement('hr'));
|
|
60
|
+
disableLink(link);
|
|
61
|
+
section = processLink(section);
|
|
62
|
+
if (section) {
|
|
63
|
+
go(section);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (rotateOrSequenceAttr) {
|
|
67
|
+
const result = rotate(rotateOrSequenceAttr, rotateAttr ? link.innerText : '');
|
|
68
|
+
link.innerHTML = result[0].replace(/"/g, '"').replace(/'/g, '\'');
|
|
69
|
+
const dataAttribute = rotateAttr ? 'data-rotate' : 'data-sequence';
|
|
70
|
+
link.setAttribute(dataAttribute, result[1] || '');
|
|
71
|
+
if (!result[1]) {
|
|
72
|
+
disableLink(link);
|
|
73
|
+
}
|
|
74
|
+
const attribute = link.getAttribute('data-attribute');
|
|
75
|
+
if (attribute) {
|
|
76
|
+
set(attribute, result[0]);
|
|
77
|
+
}
|
|
78
|
+
save();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function handleClick(event) {
|
|
82
|
+
const target = event.target;
|
|
83
|
+
if (target.classList.contains('squiffy-link')) {
|
|
84
|
+
handleLink(target);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function disableLink(link) {
|
|
88
|
+
link.classList.add('disabled');
|
|
89
|
+
link.setAttribute('tabindex', '-1');
|
|
90
|
+
}
|
|
91
|
+
function disableLinks(links) {
|
|
92
|
+
links.forEach(disableLink);
|
|
93
|
+
}
|
|
94
|
+
function begin() {
|
|
95
|
+
if (!load()) {
|
|
96
|
+
go(story.start);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function processLink(link) {
|
|
100
|
+
const sections = link.split(',');
|
|
101
|
+
let first = true;
|
|
102
|
+
let target = null;
|
|
103
|
+
sections.forEach(function (section) {
|
|
104
|
+
section = section.trim();
|
|
105
|
+
if (startsWith(section, '@replace ')) {
|
|
106
|
+
replaceLabel(section.substring(9));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
if (first) {
|
|
110
|
+
target = section;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
setAttribute(section);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
first = false;
|
|
117
|
+
});
|
|
118
|
+
return target;
|
|
119
|
+
}
|
|
120
|
+
function setAttribute(expr) {
|
|
121
|
+
expr = expr.replace(/^(\w*\s*):=(.*)$/, (_, name, value) => (name + "=" + ui.processText(value)));
|
|
122
|
+
const setRegex = /^([\w]*)\s*=\s*(.*)$/;
|
|
123
|
+
const setMatch = setRegex.exec(expr);
|
|
124
|
+
if (setMatch) {
|
|
125
|
+
const lhs = setMatch[1];
|
|
126
|
+
let rhs = setMatch[2];
|
|
127
|
+
if (isNaN(rhs)) {
|
|
128
|
+
if (startsWith(rhs, "@"))
|
|
129
|
+
rhs = get(rhs.substring(1));
|
|
130
|
+
set(lhs, rhs);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
set(lhs, parseFloat(rhs));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const incDecRegex = /^([\w]*)\s*([\+\-\*\/])=\s*(.*)$/;
|
|
138
|
+
const incDecMatch = incDecRegex.exec(expr);
|
|
139
|
+
if (incDecMatch) {
|
|
140
|
+
const lhs = incDecMatch[1];
|
|
141
|
+
const op = incDecMatch[2];
|
|
142
|
+
let rhs = incDecMatch[3];
|
|
143
|
+
if (startsWith(rhs, "@"))
|
|
144
|
+
rhs = get(rhs.substring(1));
|
|
145
|
+
const rhsNumeric = parseFloat(rhs);
|
|
146
|
+
let value = get(lhs);
|
|
147
|
+
if (value === null)
|
|
148
|
+
value = 0;
|
|
149
|
+
if (op == '+') {
|
|
150
|
+
value += rhsNumeric;
|
|
151
|
+
}
|
|
152
|
+
if (op == '-') {
|
|
153
|
+
value -= rhsNumeric;
|
|
154
|
+
}
|
|
155
|
+
if (op == '*') {
|
|
156
|
+
value *= rhsNumeric;
|
|
157
|
+
}
|
|
158
|
+
if (op == '/') {
|
|
159
|
+
value /= rhsNumeric;
|
|
160
|
+
}
|
|
161
|
+
set(lhs, value);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
let value = true;
|
|
165
|
+
if (startsWith(expr, 'not ')) {
|
|
166
|
+
expr = expr.substring(4);
|
|
167
|
+
value = false;
|
|
168
|
+
}
|
|
169
|
+
set(expr, value);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function replaceLabel(expr) {
|
|
174
|
+
const regex = /^([\w]*)\s*=\s*(.*)$/;
|
|
175
|
+
const match = regex.exec(expr);
|
|
176
|
+
if (!match)
|
|
177
|
+
return;
|
|
178
|
+
const label = match[1];
|
|
179
|
+
let text = match[2];
|
|
180
|
+
if (currentSection.passages && text in currentSection.passages) {
|
|
181
|
+
text = currentSection.passages[text].text || '';
|
|
182
|
+
}
|
|
183
|
+
else if (text in story.sections) {
|
|
184
|
+
text = story.sections[text].text || '';
|
|
185
|
+
}
|
|
186
|
+
const stripParags = /^<p>(.*)<\/p>$/;
|
|
187
|
+
const stripParagsMatch = stripParags.exec(text);
|
|
188
|
+
if (stripParagsMatch) {
|
|
189
|
+
text = stripParagsMatch[1];
|
|
190
|
+
}
|
|
191
|
+
const labelElement = outputElement.querySelector('.squiffy-label-' + label);
|
|
192
|
+
if (!labelElement)
|
|
193
|
+
return;
|
|
194
|
+
labelElement.addEventListener('transitionend', function () {
|
|
195
|
+
labelElement.innerHTML = ui.processText(text);
|
|
196
|
+
labelElement.addEventListener('transitionend', function () {
|
|
197
|
+
save();
|
|
198
|
+
}, { once: true });
|
|
199
|
+
labelElement.classList.remove('fade-out');
|
|
200
|
+
labelElement.classList.add('fade-in');
|
|
201
|
+
}, { once: true });
|
|
202
|
+
labelElement.classList.add('fade-out');
|
|
203
|
+
}
|
|
204
|
+
function go(section) {
|
|
205
|
+
set('_transition', null);
|
|
206
|
+
newSection();
|
|
207
|
+
currentSection = story.sections[section];
|
|
208
|
+
if (!currentSection)
|
|
209
|
+
return;
|
|
210
|
+
set('_section', section);
|
|
211
|
+
setSeen(section);
|
|
212
|
+
const master = story.sections[''];
|
|
213
|
+
if (master) {
|
|
214
|
+
run(master);
|
|
215
|
+
ui.write(master.text || '');
|
|
216
|
+
}
|
|
217
|
+
run(currentSection);
|
|
218
|
+
// The JS might have changed which section we're in
|
|
219
|
+
if (get('_section') == section) {
|
|
220
|
+
set('_turncount', 0);
|
|
221
|
+
ui.write(currentSection.text || '');
|
|
222
|
+
save();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function run(section) {
|
|
226
|
+
if (section.clear) {
|
|
227
|
+
ui.clearScreen();
|
|
228
|
+
}
|
|
229
|
+
if (section.attributes) {
|
|
230
|
+
processAttributes(section.attributes.map(line => line.replace(/^random\s*:\s*(\w+)\s*=\s*(.+)/i, (line, attr, options) => (options = options.split("|")) ? attr + " = " + options[Math.floor(Math.random() * options.length)] : line)));
|
|
231
|
+
}
|
|
232
|
+
if (section.jsIndex !== undefined) {
|
|
233
|
+
const squiffy = {
|
|
234
|
+
get: get,
|
|
235
|
+
set: set,
|
|
236
|
+
ui: {
|
|
237
|
+
transition: ui.transition,
|
|
238
|
+
},
|
|
239
|
+
story: {
|
|
240
|
+
go: go,
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
story.js[section.jsIndex](squiffy, get, set);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function showPassage(passageName) {
|
|
247
|
+
let passage = currentSection.passages && currentSection.passages[passageName];
|
|
248
|
+
const masterSection = story.sections[''];
|
|
249
|
+
if (!passage && masterSection && masterSection.passages)
|
|
250
|
+
passage = masterSection.passages[passageName];
|
|
251
|
+
if (!passage)
|
|
252
|
+
return;
|
|
253
|
+
setSeen(passageName);
|
|
254
|
+
if (masterSection && masterSection.passages) {
|
|
255
|
+
const masterPassage = masterSection.passages[''];
|
|
256
|
+
if (masterPassage) {
|
|
257
|
+
run(masterPassage);
|
|
258
|
+
ui.write(masterPassage.text || '');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const master = currentSection.passages && currentSection.passages[''];
|
|
262
|
+
if (master) {
|
|
263
|
+
run(master);
|
|
264
|
+
ui.write(master.text || '');
|
|
265
|
+
}
|
|
266
|
+
run(passage);
|
|
267
|
+
ui.write(passage.text || '');
|
|
268
|
+
save();
|
|
269
|
+
}
|
|
270
|
+
function processAttributes(attributes) {
|
|
271
|
+
attributes.forEach(function (attribute) {
|
|
272
|
+
if (startsWith(attribute, '@replace ')) {
|
|
273
|
+
replaceLabel(attribute.substring(9));
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
setAttribute(attribute);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function restart() {
|
|
281
|
+
if (settings.persist && window.localStorage && story.id) {
|
|
282
|
+
const keys = Object.keys(localStorage);
|
|
283
|
+
for (const key of keys) {
|
|
284
|
+
if (startsWith(key, story.id)) {
|
|
285
|
+
localStorage.removeItem(key);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
storageFallback = {};
|
|
291
|
+
}
|
|
292
|
+
if (settings.scroll === 'element') {
|
|
293
|
+
outputElement.innerHTML = '';
|
|
294
|
+
begin();
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
location.reload();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function save() {
|
|
301
|
+
set('_output', outputElement.innerHTML);
|
|
302
|
+
}
|
|
303
|
+
function load() {
|
|
304
|
+
const output = get('_output');
|
|
305
|
+
if (!output)
|
|
306
|
+
return false;
|
|
307
|
+
outputElement.innerHTML = output;
|
|
308
|
+
const element = document.getElementById(get('_output-section'));
|
|
309
|
+
if (!element)
|
|
310
|
+
return false;
|
|
311
|
+
currentSectionElement = element;
|
|
312
|
+
currentSection = story.sections[get('_section')];
|
|
313
|
+
const transition = get('_transition');
|
|
314
|
+
if (transition) {
|
|
315
|
+
eval('(' + transition + ')()');
|
|
316
|
+
}
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
function setSeen(sectionName) {
|
|
320
|
+
let seenSections = get('_seen_sections');
|
|
321
|
+
if (!seenSections)
|
|
322
|
+
seenSections = [];
|
|
323
|
+
if (seenSections.indexOf(sectionName) == -1) {
|
|
324
|
+
seenSections.push(sectionName);
|
|
325
|
+
set('_seen_sections', seenSections);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function seen(sectionName) {
|
|
329
|
+
const seenSections = get('_seen_sections');
|
|
330
|
+
if (!seenSections)
|
|
331
|
+
return false;
|
|
332
|
+
return (seenSections.indexOf(sectionName) > -1);
|
|
333
|
+
}
|
|
334
|
+
function newSection() {
|
|
335
|
+
if (currentSectionElement) {
|
|
336
|
+
disableLinks(currentSectionElement.querySelectorAll('.squiffy-link'));
|
|
337
|
+
currentSectionElement.querySelectorAll('input').forEach(el => {
|
|
338
|
+
const attribute = el.getAttribute('data-attribute') || el.id;
|
|
339
|
+
if (attribute)
|
|
340
|
+
set(attribute, el.value);
|
|
341
|
+
el.disabled = true;
|
|
342
|
+
});
|
|
343
|
+
currentSectionElement.querySelectorAll("[contenteditable]").forEach(el => {
|
|
344
|
+
const attribute = el.getAttribute('data-attribute') || el.id;
|
|
345
|
+
if (attribute)
|
|
346
|
+
set(attribute, el.innerHTML);
|
|
347
|
+
el.contentEditable = 'false';
|
|
348
|
+
});
|
|
349
|
+
currentSectionElement.querySelectorAll('textarea').forEach(el => {
|
|
350
|
+
const attribute = el.getAttribute('data-attribute') || el.id;
|
|
351
|
+
if (attribute)
|
|
352
|
+
set(attribute, el.value);
|
|
353
|
+
el.disabled = true;
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
const sectionCount = get('_section-count') + 1;
|
|
357
|
+
set('_section-count', sectionCount);
|
|
358
|
+
const id = 'squiffy-section-' + sectionCount;
|
|
359
|
+
currentSectionElement = document.createElement('div');
|
|
360
|
+
currentSectionElement.id = id;
|
|
361
|
+
outputElement.appendChild(currentSectionElement);
|
|
362
|
+
set('_output-section', id);
|
|
363
|
+
}
|
|
364
|
+
const ui = {
|
|
365
|
+
write: (text) => {
|
|
366
|
+
if (!currentSectionElement)
|
|
367
|
+
return;
|
|
368
|
+
scrollPosition = outputElement.scrollHeight;
|
|
369
|
+
const div = document.createElement('div');
|
|
370
|
+
currentSectionElement.appendChild(div);
|
|
371
|
+
div.innerHTML = ui.processText(text);
|
|
372
|
+
ui.scrollToEnd();
|
|
373
|
+
},
|
|
374
|
+
clearScreen: () => {
|
|
375
|
+
outputElement.innerHTML = '';
|
|
376
|
+
newSection();
|
|
377
|
+
},
|
|
378
|
+
scrollToEnd: () => {
|
|
379
|
+
if (settings.scroll === 'element') {
|
|
380
|
+
const scrollTo = outputElement.scrollHeight - outputElement.clientHeight;
|
|
381
|
+
const currentScrollTop = outputElement.scrollTop;
|
|
382
|
+
if (scrollTo > (currentScrollTop || 0)) {
|
|
383
|
+
outputElement.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
let scrollTo = scrollPosition;
|
|
388
|
+
const currentScrollTop = Math.max(document.body.scrollTop, document.documentElement.scrollTop);
|
|
389
|
+
if (scrollTo > currentScrollTop) {
|
|
390
|
+
const maxScrollTop = document.documentElement.scrollHeight - window.innerHeight;
|
|
391
|
+
if (scrollTo > maxScrollTop)
|
|
392
|
+
scrollTo = maxScrollTop;
|
|
393
|
+
window.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
processText: (text) => {
|
|
398
|
+
function process(text, data) {
|
|
399
|
+
let containsUnprocessedSection = false;
|
|
400
|
+
const open = text.indexOf('{');
|
|
401
|
+
let close;
|
|
402
|
+
if (open > -1) {
|
|
403
|
+
let nestCount = 1;
|
|
404
|
+
let searchStart = open + 1;
|
|
405
|
+
let finished = false;
|
|
406
|
+
while (!finished) {
|
|
407
|
+
const nextOpen = text.indexOf('{', searchStart);
|
|
408
|
+
const nextClose = text.indexOf('}', searchStart);
|
|
409
|
+
if (nextClose > -1) {
|
|
410
|
+
if (nextOpen > -1 && nextOpen < nextClose) {
|
|
411
|
+
nestCount++;
|
|
412
|
+
searchStart = nextOpen + 1;
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
nestCount--;
|
|
416
|
+
searchStart = nextClose + 1;
|
|
417
|
+
if (nestCount === 0) {
|
|
418
|
+
close = nextClose;
|
|
419
|
+
containsUnprocessedSection = true;
|
|
420
|
+
finished = true;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
finished = true;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (containsUnprocessedSection) {
|
|
430
|
+
const section = text.substring(open + 1, close);
|
|
431
|
+
const value = processTextCommand(section, data);
|
|
432
|
+
text = text.substring(0, open) + value + process(text.substring(close + 1), data);
|
|
433
|
+
}
|
|
434
|
+
return (text);
|
|
435
|
+
}
|
|
436
|
+
function processTextCommand(text, data) {
|
|
437
|
+
if (startsWith(text, 'if ')) {
|
|
438
|
+
return processTextCommand_If(text, data);
|
|
439
|
+
}
|
|
440
|
+
else if (startsWith(text, 'else:')) {
|
|
441
|
+
return processTextCommand_Else(text, data);
|
|
442
|
+
}
|
|
443
|
+
else if (startsWith(text, 'label:')) {
|
|
444
|
+
return processTextCommand_Label(text, data);
|
|
445
|
+
}
|
|
446
|
+
else if (/^rotate[: ]/.test(text)) {
|
|
447
|
+
return processTextCommand_Rotate('rotate', text);
|
|
448
|
+
}
|
|
449
|
+
else if (/^sequence[: ]/.test(text)) {
|
|
450
|
+
return processTextCommand_Rotate('sequence', text);
|
|
451
|
+
}
|
|
452
|
+
else if (currentSection.passages && text in currentSection.passages) {
|
|
453
|
+
return process(currentSection.passages[text].text || '', data);
|
|
454
|
+
}
|
|
455
|
+
else if (text in story.sections) {
|
|
456
|
+
return process(story.sections[text].text || '', data);
|
|
457
|
+
}
|
|
458
|
+
else if (startsWith(text, '@') && !startsWith(text, '@replace')) {
|
|
459
|
+
processAttributes(text.substring(1).split(","));
|
|
460
|
+
return "";
|
|
461
|
+
}
|
|
462
|
+
return get(text);
|
|
463
|
+
}
|
|
464
|
+
function processTextCommand_If(section, data) {
|
|
465
|
+
const command = section.substring(3);
|
|
466
|
+
const colon = command.indexOf(':');
|
|
467
|
+
if (colon == -1) {
|
|
468
|
+
return ('{if ' + command + '}');
|
|
469
|
+
}
|
|
470
|
+
const text = command.substring(colon + 1);
|
|
471
|
+
let condition = command.substring(0, colon);
|
|
472
|
+
condition = condition.replace("<", "<");
|
|
473
|
+
const operatorRegex = /([\w ]*)(=|<=|>=|<>|<|>)(.*)/;
|
|
474
|
+
const match = operatorRegex.exec(condition);
|
|
475
|
+
let result = false;
|
|
476
|
+
if (match) {
|
|
477
|
+
const lhs = get(match[1]);
|
|
478
|
+
const op = match[2];
|
|
479
|
+
let rhs = match[3];
|
|
480
|
+
if (startsWith(rhs, '@'))
|
|
481
|
+
rhs = get(rhs.substring(1));
|
|
482
|
+
if (op == '=' && lhs == rhs)
|
|
483
|
+
result = true;
|
|
484
|
+
if (op == '<>' && lhs != rhs)
|
|
485
|
+
result = true;
|
|
486
|
+
if (op == '>' && lhs > rhs)
|
|
487
|
+
result = true;
|
|
488
|
+
if (op == '<' && lhs < rhs)
|
|
489
|
+
result = true;
|
|
490
|
+
if (op == '>=' && lhs >= rhs)
|
|
491
|
+
result = true;
|
|
492
|
+
if (op == '<=' && lhs <= rhs)
|
|
493
|
+
result = true;
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
let checkValue = true;
|
|
497
|
+
if (startsWith(condition, 'not ')) {
|
|
498
|
+
condition = condition.substring(4);
|
|
499
|
+
checkValue = false;
|
|
500
|
+
}
|
|
501
|
+
if (startsWith(condition, 'seen ')) {
|
|
502
|
+
result = (seen(condition.substring(5)) == checkValue);
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
let value = get(condition);
|
|
506
|
+
if (value === null)
|
|
507
|
+
value = false;
|
|
508
|
+
result = (value == checkValue);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const textResult = result ? process(text, data) : '';
|
|
512
|
+
data.lastIf = result;
|
|
513
|
+
return textResult;
|
|
514
|
+
}
|
|
515
|
+
function processTextCommand_Else(section, data) {
|
|
516
|
+
if (!('lastIf' in data) || data.lastIf)
|
|
517
|
+
return '';
|
|
518
|
+
const text = section.substring(5);
|
|
519
|
+
return process(text, data);
|
|
520
|
+
}
|
|
521
|
+
function processTextCommand_Label(section, data) {
|
|
522
|
+
const command = section.substring(6);
|
|
523
|
+
const eq = command.indexOf('=');
|
|
524
|
+
if (eq == -1) {
|
|
525
|
+
return ('{label:' + command + '}');
|
|
526
|
+
}
|
|
527
|
+
const text = command.substring(eq + 1);
|
|
528
|
+
const label = command.substring(0, eq);
|
|
529
|
+
return '<span class="squiffy-label-' + label + '">' + process(text, data) + '</span>';
|
|
530
|
+
}
|
|
531
|
+
function processTextCommand_Rotate(type, section) {
|
|
532
|
+
let options;
|
|
533
|
+
let attribute = '';
|
|
534
|
+
if (section.substring(type.length, type.length + 1) == ' ') {
|
|
535
|
+
const colon = section.indexOf(':');
|
|
536
|
+
if (colon == -1) {
|
|
537
|
+
return '{' + section + '}';
|
|
538
|
+
}
|
|
539
|
+
options = section.substring(colon + 1);
|
|
540
|
+
attribute = section.substring(type.length + 1, colon);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
options = section.substring(type.length + 1);
|
|
544
|
+
}
|
|
545
|
+
// TODO: Check - previously there was no second parameter here
|
|
546
|
+
const rotation = rotate(options.replace(/"/g, '"').replace(/'/g, '''), null);
|
|
547
|
+
if (attribute) {
|
|
548
|
+
set(attribute, rotation[0]);
|
|
549
|
+
}
|
|
550
|
+
return '<a class="squiffy-link" data-' + type + '="' + rotation[1] + '" data-attribute="' + attribute + '" role="link">' + rotation[0] + '</a>';
|
|
551
|
+
}
|
|
552
|
+
const data = {
|
|
553
|
+
fulltext: text
|
|
554
|
+
};
|
|
555
|
+
return process(text, data);
|
|
556
|
+
},
|
|
557
|
+
transition: function (f) {
|
|
558
|
+
set('_transition', f.toString());
|
|
559
|
+
f();
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
function startsWith(string, prefix) {
|
|
563
|
+
return string.substring(0, prefix.length) === prefix;
|
|
564
|
+
}
|
|
565
|
+
function rotate(options, current) {
|
|
566
|
+
const colon = options.indexOf(':');
|
|
567
|
+
if (colon == -1) {
|
|
568
|
+
return [options, current];
|
|
569
|
+
}
|
|
570
|
+
const next = options.substring(0, colon);
|
|
571
|
+
let remaining = options.substring(colon + 1);
|
|
572
|
+
if (current)
|
|
573
|
+
remaining += ':' + current;
|
|
574
|
+
return [next, remaining];
|
|
575
|
+
}
|
|
576
|
+
outputElement = options.element;
|
|
577
|
+
story = options.story;
|
|
578
|
+
settings = {
|
|
579
|
+
scroll: options.scroll || 'body',
|
|
580
|
+
persist: (options.persist === undefined) ? true : options.persist,
|
|
581
|
+
onSet: options.onSet || (() => { })
|
|
582
|
+
};
|
|
583
|
+
if (options.persist === true && !story.id) {
|
|
584
|
+
console.warn("Persist is set to true in Squiffy runtime options, but no story id has been set. Persist will be disabled.");
|
|
585
|
+
settings.persist = false;
|
|
586
|
+
}
|
|
587
|
+
if (settings.scroll === 'element') {
|
|
588
|
+
outputElement.style.overflowY = 'auto';
|
|
589
|
+
}
|
|
590
|
+
document.addEventListener('click', handleClick);
|
|
591
|
+
document.addEventListener('keypress', function (event) {
|
|
592
|
+
if (event.key !== "Enter")
|
|
593
|
+
return;
|
|
594
|
+
handleClick(event);
|
|
595
|
+
});
|
|
596
|
+
begin();
|
|
597
|
+
return {
|
|
598
|
+
restart: restart,
|
|
599
|
+
get: get,
|
|
600
|
+
set: set,
|
|
601
|
+
clickLink: handleLink
|
|
602
|
+
};
|
|
603
|
+
};
|
package/package.json
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squiffy-runtime",
|
|
3
|
-
"version": "6.0.0-alpha.
|
|
4
|
-
"main": "squiffy.runtime.js",
|
|
3
|
+
"version": "6.0.0-alpha.3",
|
|
4
|
+
"main": "dist/squiffy.runtime.js",
|
|
5
|
+
"types": "dist/squiffy.runtime.d.ts",
|
|
5
6
|
"scripts": {
|
|
6
|
-
"test": "
|
|
7
|
+
"test": "vitest",
|
|
7
8
|
"build": "tsc"
|
|
8
9
|
},
|
|
9
10
|
"author": "Alex Warren",
|
|
11
|
+
"contributors": [
|
|
12
|
+
"CrisisSDK",
|
|
13
|
+
"mrangel",
|
|
14
|
+
"Luis Felipe Morales"
|
|
15
|
+
],
|
|
10
16
|
"license": "MIT",
|
|
11
17
|
"description": "",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/jsdom": "^21.1.7",
|
|
20
|
+
"global-jsdom": "^25.0.0",
|
|
21
|
+
"jsdom": "^25.0.0",
|
|
22
|
+
"squiffy-compiler": "^6.0.0-alpha.2",
|
|
23
|
+
"typescript": "^5.6.2",
|
|
24
|
+
"vitest": "^2.1.1"
|
|
14
25
|
}
|
|
15
26
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`"Hello world" script should run 1`] = `
|
|
4
|
+
"
|
|
5
|
+
<div id="squiffy-section-1"><div><p>Hello world</p></div></div>"
|
|
6
|
+
`;
|
|
7
|
+
|
|
8
|
+
exports[`Click a passage link 1`] = `
|
|
9
|
+
"
|
|
10
|
+
<div id="squiffy-section-1"><div><h1>Squiffy Test</h1>
|
|
11
|
+
<p>This is <a href="http://textadventures.co.uk">a website link</a>.</p>
|
|
12
|
+
<p>This should be <a class="squiffy-link link-passage" data-passage="passage" role="link" tabindex="0">a link to a passage</a>. Here's <a class="squiffy-link link-passage" data-passage="passage2" role="link" tabindex="0">another one</a>.</p>
|
|
13
|
+
<p>You don't need to specify a name - for example, this <a class="squiffy-link link-passage" data-passage="link" role="link" tabindex="0">link</a> and this <a class="squiffy-link link-section" data-section="section" role="link" tabindex="0">section</a>.</p>
|
|
14
|
+
<p>And this goes to the <a class="squiffy-link link-section" data-section="section2" role="link" tabindex="0">next section</a>.</p>
|
|
15
|
+
<p>This line has links to <a class="squiffy-link link-section" data-section="section 3" role="link" tabindex="0">section 3</a> and <a class="squiffy-link link-section" data-section="section four" role="link" tabindex="0">section 4</a>.</p>
|
|
16
|
+
<p>This line has links to <a class="squiffy-link link-passage" data-passage="passage 3" role="link" tabindex="0">passage 3</a> and <a class="squiffy-link link-passage" data-passage="passage four" role="link" tabindex="0">passage 4</a>.</p>
|
|
17
|
+
<p>Oh look - <a class="squiffy-link link-passage" data-passage="it's a passage with an apostrophe" role="link" tabindex="0">it's a passage with an apostrophe</a>.</p></div></div>"
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
exports[`Click a passage link 2`] = `
|
|
21
|
+
"
|
|
22
|
+
<div id="squiffy-section-1"><div><h1>Squiffy Test</h1>
|
|
23
|
+
<p>This is <a href="http://textadventures.co.uk">a website link</a>.</p>
|
|
24
|
+
<p>This should be <a class="squiffy-link link-passage disabled" data-passage="passage" role="link" tabindex="-1">a link to a passage</a>. Here's <a class="squiffy-link link-passage" data-passage="passage2" role="link" tabindex="0">another one</a>.</p>
|
|
25
|
+
<p>You don't need to specify a name - for example, this <a class="squiffy-link link-passage" data-passage="link" role="link" tabindex="0">link</a> and this <a class="squiffy-link link-section" data-section="section" role="link" tabindex="0">section</a>.</p>
|
|
26
|
+
<p>And this goes to the <a class="squiffy-link link-section" data-section="section2" role="link" tabindex="0">next section</a>.</p>
|
|
27
|
+
<p>This line has links to <a class="squiffy-link link-section" data-section="section 3" role="link" tabindex="0">section 3</a> and <a class="squiffy-link link-section" data-section="section four" role="link" tabindex="0">section 4</a>.</p>
|
|
28
|
+
<p>This line has links to <a class="squiffy-link link-passage" data-passage="passage 3" role="link" tabindex="0">passage 3</a> and <a class="squiffy-link link-passage" data-passage="passage four" role="link" tabindex="0">passage 4</a>.</p>
|
|
29
|
+
<p>Oh look - <a class="squiffy-link link-passage" data-passage="it's a passage with an apostrophe" role="link" tabindex="0">it's a passage with an apostrophe</a>.</p></div><hr><div><p>Here's some text for the passage.</p></div></div>"
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
exports[`Click a section link 1`] = `
|
|
33
|
+
"
|
|
34
|
+
<div id="squiffy-section-1"><div><h1>Squiffy Test</h1>
|
|
35
|
+
<p>This is <a href="http://textadventures.co.uk">a website link</a>.</p>
|
|
36
|
+
<p>This should be <a class="squiffy-link link-passage" data-passage="passage" role="link" tabindex="0">a link to a passage</a>. Here's <a class="squiffy-link link-passage" data-passage="passage2" role="link" tabindex="0">another one</a>.</p>
|
|
37
|
+
<p>You don't need to specify a name - for example, this <a class="squiffy-link link-passage" data-passage="link" role="link" tabindex="0">link</a> and this <a class="squiffy-link link-section" data-section="section" role="link" tabindex="0">section</a>.</p>
|
|
38
|
+
<p>And this goes to the <a class="squiffy-link link-section" data-section="section2" role="link" tabindex="0">next section</a>.</p>
|
|
39
|
+
<p>This line has links to <a class="squiffy-link link-section" data-section="section 3" role="link" tabindex="0">section 3</a> and <a class="squiffy-link link-section" data-section="section four" role="link" tabindex="0">section 4</a>.</p>
|
|
40
|
+
<p>This line has links to <a class="squiffy-link link-passage" data-passage="passage 3" role="link" tabindex="0">passage 3</a> and <a class="squiffy-link link-passage" data-passage="passage four" role="link" tabindex="0">passage 4</a>.</p>
|
|
41
|
+
<p>Oh look - <a class="squiffy-link link-passage" data-passage="it's a passage with an apostrophe" role="link" tabindex="0">it's a passage with an apostrophe</a>.</p></div></div>"
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
exports[`Click a section link 2`] = `
|
|
45
|
+
"
|
|
46
|
+
<div id="squiffy-section-1"><div><h1>Squiffy Test</h1>
|
|
47
|
+
<p>This is <a href="http://textadventures.co.uk">a website link</a>.</p>
|
|
48
|
+
<p>This should be <a class="squiffy-link link-passage disabled" data-passage="passage" role="link" tabindex="-1">a link to a passage</a>. Here's <a class="squiffy-link link-passage disabled" data-passage="passage2" role="link" tabindex="-1">another one</a>.</p>
|
|
49
|
+
<p>You don't need to specify a name - for example, this <a class="squiffy-link link-passage disabled" data-passage="link" role="link" tabindex="-1">link</a> and this <a class="squiffy-link link-section disabled" data-section="section" role="link" tabindex="-1">section</a>.</p>
|
|
50
|
+
<p>And this goes to the <a class="squiffy-link link-section disabled" data-section="section2" role="link" tabindex="-1">next section</a>.</p>
|
|
51
|
+
<p>This line has links to <a class="squiffy-link link-section disabled" data-section="section 3" role="link" tabindex="-1">section 3</a> and <a class="squiffy-link link-section disabled" data-section="section four" role="link" tabindex="-1">section 4</a>.</p>
|
|
52
|
+
<p>This line has links to <a class="squiffy-link link-passage disabled" data-passage="passage 3" role="link" tabindex="-1">passage 3</a> and <a class="squiffy-link link-passage disabled" data-passage="passage four" role="link" tabindex="-1">passage 4</a>.</p>
|
|
53
|
+
<p>Oh look - <a class="squiffy-link link-passage disabled" data-passage="it's a passage with an apostrophe" role="link" tabindex="-1">it's a passage with an apostrophe</a>.</p></div><hr></div><div id="squiffy-section-2"><div><p>Another section is here.</p></div></div>"
|
|
54
|
+
`;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { expect, test, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import globalJsdom from 'global-jsdom';
|
|
4
|
+
import { init } from './squiffy.runtime.js';
|
|
5
|
+
import { compile } from 'squiffy-compiler';
|
|
6
|
+
|
|
7
|
+
const html = `
|
|
8
|
+
<!DOCTYPE html>
|
|
9
|
+
<html>
|
|
10
|
+
<head>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="squiffy">
|
|
14
|
+
</div>
|
|
15
|
+
<div id="test">
|
|
16
|
+
</div>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const initScript = async (script: string) => {
|
|
22
|
+
globalJsdom(html);
|
|
23
|
+
const element = document.getElementById('squiffy');
|
|
24
|
+
|
|
25
|
+
if (!element) {
|
|
26
|
+
throw new Error('Element not found');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const compileResult = await compile({
|
|
30
|
+
scriptBaseFilename: "filename.squiffy", // TODO: This shouldn't be required
|
|
31
|
+
script: script,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!compileResult.success) {
|
|
35
|
+
throw new Error('Compile failed');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const story = compileResult.output.story;
|
|
39
|
+
const js = compileResult.output.js.map(jsLines => new Function('squiffy', 'get', 'set', jsLines.join('\n')));
|
|
40
|
+
|
|
41
|
+
const squiffyApi = init({
|
|
42
|
+
element: element,
|
|
43
|
+
story: {
|
|
44
|
+
js: js as any,
|
|
45
|
+
...story,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
squiffyApi,
|
|
51
|
+
element
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const findLink = (element: HTMLElement, linkType: string, linkText: string, onlyEnabled: boolean = false) => {
|
|
56
|
+
const links = element.querySelectorAll(`a.squiffy-link.link-${linkType}`);
|
|
57
|
+
return Array.from(links).find(link => link.textContent === linkText && (onlyEnabled ? !link.classList.contains("disabled") : true)) as HTMLElement;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const getTestOutput = () => {
|
|
61
|
+
const testElement = document.getElementById('test');
|
|
62
|
+
if (!testElement) {
|
|
63
|
+
throw new Error('Test element not found');
|
|
64
|
+
}
|
|
65
|
+
return testElement.innerText;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let cleanup: { (): void };
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
cleanup = globalJsdom();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
cleanup();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('"Hello world" script should run', async () => {
|
|
79
|
+
const { element } = await initScript("Hello world");
|
|
80
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('Click a section link', async () => {
|
|
84
|
+
const script = await fs.readFile('../examples/test/example.squiffy', 'utf-8');
|
|
85
|
+
const { squiffyApi, element } = await initScript(script);
|
|
86
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
87
|
+
|
|
88
|
+
expect(element.querySelectorAll('a.squiffy-link').length).toBe(10);
|
|
89
|
+
const linkToPassage = findLink(element, 'passage', 'a link to a passage');
|
|
90
|
+
const section3Link = findLink(element, 'section', 'section 3');
|
|
91
|
+
|
|
92
|
+
expect(linkToPassage).toBeDefined();
|
|
93
|
+
expect(section3Link).toBeDefined();
|
|
94
|
+
expect(linkToPassage.classList).not.toContain('disabled');
|
|
95
|
+
expect(section3Link.classList).not.toContain('disabled');
|
|
96
|
+
|
|
97
|
+
squiffyApi.clickLink(section3Link);
|
|
98
|
+
|
|
99
|
+
expect(linkToPassage.classList).toContain('disabled');
|
|
100
|
+
expect(section3Link.classList).toContain('disabled');
|
|
101
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('Click a passage link', async () => {
|
|
105
|
+
const script = await fs.readFile('../examples/test/example.squiffy', 'utf-8');
|
|
106
|
+
const { squiffyApi, element } = await initScript(script);
|
|
107
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
108
|
+
|
|
109
|
+
expect(element.querySelectorAll('a.squiffy-link').length).toBe(10);
|
|
110
|
+
const linkToPassage = findLink(element, 'passage', 'a link to a passage');
|
|
111
|
+
const section3Link = findLink(element, 'section', 'section 3');
|
|
112
|
+
|
|
113
|
+
expect(linkToPassage).toBeDefined();
|
|
114
|
+
expect(section3Link).toBeDefined();
|
|
115
|
+
expect(linkToPassage.classList).not.toContain('disabled');
|
|
116
|
+
expect(section3Link.classList).not.toContain('disabled');
|
|
117
|
+
|
|
118
|
+
squiffyApi.clickLink(linkToPassage);
|
|
119
|
+
|
|
120
|
+
expect(linkToPassage.classList).toContain('disabled');
|
|
121
|
+
expect(section3Link.classList).not.toContain('disabled');
|
|
122
|
+
expect(element.innerHTML).toMatchSnapshot();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('Run JavaScript functions', async () => {
|
|
126
|
+
const script = `
|
|
127
|
+
document.getElementById('test').innerText = 'Initial JavaScript';
|
|
128
|
+
@set some_string = some_value
|
|
129
|
+
@set some_number = 5
|
|
130
|
+
|
|
131
|
+
+++Continue...
|
|
132
|
+
document.getElementById('test').innerText = "Value: " + get("some_number");
|
|
133
|
+
+++Continue...
|
|
134
|
+
document.getElementById('test').innerText = "Value: " + get("some_string");
|
|
135
|
+
set("some_number", 10);
|
|
136
|
+
+++Continue...
|
|
137
|
+
document.getElementById('test').innerText = "Value: " + get("some_number");
|
|
138
|
+
+++Continue...
|
|
139
|
+
@inc some_number
|
|
140
|
+
+++Continue...
|
|
141
|
+
document.getElementById('test').innerText = "Value: " + get("some_number");
|
|
142
|
+
+++Continue...
|
|
143
|
+
squiffy.story.go("other section");
|
|
144
|
+
[[other section]]:
|
|
145
|
+
document.getElementById('test').innerText = "In other section";
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
const clickContinue = () => {
|
|
149
|
+
const continueLink = findLink(element, 'section', 'Continue...', true);
|
|
150
|
+
expect(continueLink).toBeDefined();
|
|
151
|
+
squiffyApi.clickLink(continueLink);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const { squiffyApi, element } = await initScript(script);
|
|
155
|
+
|
|
156
|
+
expect(getTestOutput()).toBe('Initial JavaScript');
|
|
157
|
+
clickContinue();
|
|
158
|
+
|
|
159
|
+
expect(getTestOutput()).toBe('Value: 5');
|
|
160
|
+
clickContinue();
|
|
161
|
+
|
|
162
|
+
expect(getTestOutput()).toBe('Value: some_value');
|
|
163
|
+
clickContinue();
|
|
164
|
+
|
|
165
|
+
expect(getTestOutput()).toBe('Value: 10');
|
|
166
|
+
|
|
167
|
+
clickContinue();
|
|
168
|
+
clickContinue();
|
|
169
|
+
expect(getTestOutput()).toBe('Value: 11');
|
|
170
|
+
|
|
171
|
+
clickContinue();
|
|
172
|
+
expect(getTestOutput()).toBe('In other section');
|
|
173
|
+
});
|
package/src/squiffy.runtime.ts
CHANGED
|
@@ -16,10 +16,28 @@ interface SquiffyApi {
|
|
|
16
16
|
restart: () => void;
|
|
17
17
|
get: (attribute: string) => any;
|
|
18
18
|
set: (attribute: string, value: any) => void;
|
|
19
|
+
clickLink: (link: HTMLElement) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Previous versions of Squiffy had "squiffy", "get" and "set" as globals - we now pass these directly into JS functions.
|
|
23
|
+
// We may tidy up this API at some point, though that would be a breaking change.
|
|
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
|
+
};
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
interface Story {
|
|
22
|
-
js: ((
|
|
36
|
+
js: ((
|
|
37
|
+
squiffy: SquiffyJsFunctionApi,
|
|
38
|
+
get: (attribute: string) => any,
|
|
39
|
+
set: (attribute: string, value: any) => void
|
|
40
|
+
) => void)[];
|
|
23
41
|
start: string;
|
|
24
42
|
id?: string | null;
|
|
25
43
|
sections: Record<string, Section>;
|
|
@@ -73,68 +91,60 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
73
91
|
return JSON.parse(result);
|
|
74
92
|
}
|
|
75
93
|
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
function handleLink(link: HTMLElement) {
|
|
95
|
+
if (link.classList.contains('disabled')) return;
|
|
96
|
+
let passage = link.getAttribute('data-passage');
|
|
97
|
+
let section = link.getAttribute('data-section');
|
|
98
|
+
const rotateAttr = link.getAttribute('data-rotate');
|
|
99
|
+
const sequenceAttr = link.getAttribute('data-sequence');
|
|
100
|
+
const rotateOrSequenceAttr = rotateAttr || sequenceAttr;
|
|
101
|
+
if (passage) {
|
|
102
|
+
disableLink(link);
|
|
103
|
+
set('_turncount', get('_turncount') + 1);
|
|
104
|
+
passage = processLink(passage);
|
|
84
105
|
if (passage) {
|
|
85
|
-
disableLink(link);
|
|
86
|
-
set('_turncount', get('_turncount') + 1);
|
|
87
|
-
passage = processLink(passage);
|
|
88
|
-
if (passage) {
|
|
89
|
-
currentSectionElement?.appendChild(document.createElement('hr'));
|
|
90
|
-
showPassage(passage);
|
|
91
|
-
}
|
|
92
|
-
const turnPassage = '@' + get('_turncount');
|
|
93
|
-
if (currentSection.passages) {
|
|
94
|
-
if (turnPassage in currentSection.passages) {
|
|
95
|
-
showPassage(turnPassage);
|
|
96
|
-
}
|
|
97
|
-
if ('@last' in currentSection.passages && get('_turncount') >= (currentSection.passageCount || 0)) {
|
|
98
|
-
showPassage('@last');
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
else if (section) {
|
|
103
106
|
currentSectionElement?.appendChild(document.createElement('hr'));
|
|
104
|
-
|
|
105
|
-
section = processLink(section);
|
|
106
|
-
if (section) {
|
|
107
|
-
go(section);
|
|
108
|
-
}
|
|
107
|
+
showPassage(passage);
|
|
109
108
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
link.setAttribute(dataAttribute, result[1] || '');
|
|
115
|
-
if (!result[1]) {
|
|
116
|
-
disableLink(link);
|
|
109
|
+
const turnPassage = '@' + get('_turncount');
|
|
110
|
+
if (currentSection.passages) {
|
|
111
|
+
if (turnPassage in currentSection.passages) {
|
|
112
|
+
showPassage(turnPassage);
|
|
117
113
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
set(attribute, result[0]);
|
|
114
|
+
if ('@last' in currentSection.passages && get('_turncount') >= (currentSection.passageCount || 0)) {
|
|
115
|
+
showPassage('@last');
|
|
121
116
|
}
|
|
122
|
-
save();
|
|
123
117
|
}
|
|
124
118
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
119
|
+
else if (section) {
|
|
120
|
+
currentSectionElement?.appendChild(document.createElement('hr'));
|
|
121
|
+
disableLink(link);
|
|
122
|
+
section = processLink(section);
|
|
123
|
+
if (section) {
|
|
124
|
+
go(section);
|
|
130
125
|
}
|
|
131
126
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
127
|
+
else if (rotateOrSequenceAttr) {
|
|
128
|
+
const result = rotate(rotateOrSequenceAttr, rotateAttr ? link.innerText : '');
|
|
129
|
+
link.innerHTML = result[0]!.replace(/"/g, '"').replace(/'/g, '\'');
|
|
130
|
+
const dataAttribute = rotateAttr ? 'data-rotate' : 'data-sequence';
|
|
131
|
+
link.setAttribute(dataAttribute, result[1] || '');
|
|
132
|
+
if (!result[1]) {
|
|
133
|
+
disableLink(link);
|
|
134
|
+
}
|
|
135
|
+
const attribute = link.getAttribute('data-attribute');
|
|
136
|
+
if (attribute) {
|
|
137
|
+
set(attribute, result[0]);
|
|
138
|
+
}
|
|
139
|
+
save();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function handleClick(event: Event) {
|
|
144
|
+
const target = event.target as HTMLElement;
|
|
145
|
+
if (target.classList.contains('squiffy-link')) {
|
|
146
|
+
handleLink(target);
|
|
147
|
+
}
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
function disableLink(link: Element) {
|
|
@@ -289,7 +299,17 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
289
299
|
processAttributes(section.attributes.map(line => line.replace(/^random\s*:\s*(\w+)\s*=\s*(.+)/i, (line, attr, options) => (options = options.split("|")) ? attr + " = " + options[Math.floor(Math.random() * options.length)] : line)));
|
|
290
300
|
}
|
|
291
301
|
if (section.jsIndex !== undefined) {
|
|
292
|
-
|
|
302
|
+
const squiffy = {
|
|
303
|
+
get: get,
|
|
304
|
+
set: set,
|
|
305
|
+
ui: {
|
|
306
|
+
transition: ui.transition,
|
|
307
|
+
},
|
|
308
|
+
story: {
|
|
309
|
+
go: go,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
story.js[section.jsIndex](squiffy, get, set);
|
|
293
313
|
}
|
|
294
314
|
}
|
|
295
315
|
|
|
@@ -659,12 +679,18 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
659
679
|
outputElement.style.overflowY = 'auto';
|
|
660
680
|
}
|
|
661
681
|
|
|
662
|
-
|
|
682
|
+
document.addEventListener('click', handleClick);
|
|
683
|
+
document.addEventListener('keypress', function (event) {
|
|
684
|
+
if (event.key !== "Enter") return;
|
|
685
|
+
handleClick(event);
|
|
686
|
+
});
|
|
687
|
+
|
|
663
688
|
begin();
|
|
664
689
|
|
|
665
690
|
return {
|
|
666
691
|
restart: restart,
|
|
667
692
|
get: get,
|
|
668
693
|
set: set,
|
|
694
|
+
clickLink: handleLink
|
|
669
695
|
};
|
|
670
696
|
};
|