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
package/src/squiffy.runtime.ts
CHANGED
|
@@ -1,54 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
persist?: boolean,
|
|
6
|
-
onSet?: (attribute: string, value: any) => void,
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface SquiffySettings {
|
|
10
|
-
scroll: string,
|
|
11
|
-
persist: boolean,
|
|
12
|
-
onSet: (attribute: string, value: any) => void,
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface SquiffyApi {
|
|
16
|
-
restart: () => void;
|
|
17
|
-
get: (attribute: string) => any;
|
|
18
|
-
set: (attribute: string, value: any) => void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface Story {
|
|
22
|
-
js: (() => void)[];
|
|
23
|
-
start: string;
|
|
24
|
-
id?: string | null;
|
|
25
|
-
sections: Record<string, Section>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface Section {
|
|
29
|
-
text?: string;
|
|
30
|
-
clear?: boolean;
|
|
31
|
-
attributes?: string[],
|
|
32
|
-
jsIndex?: number;
|
|
33
|
-
passages?: Record<string, Passage>;
|
|
34
|
-
passageCount?: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface Passage {
|
|
38
|
-
text?: string;
|
|
39
|
-
clear?: boolean;
|
|
40
|
-
attributes?: string[];
|
|
41
|
-
jsIndex?: number;
|
|
42
|
-
}
|
|
1
|
+
import { SquiffyApi, SquiffyInitOptions, SquiffySettings, Story, Section } from './types.js';
|
|
2
|
+
import { startsWith, rotate } from "./utils.js";
|
|
3
|
+
import { TextProcessor } from './textProcessor.js';
|
|
4
|
+
import { Emitter, SquiffyEventMap } from './events.js';
|
|
43
5
|
|
|
44
6
|
export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
45
7
|
let story: Story;
|
|
46
8
|
let currentSection: Section;
|
|
47
9
|
let currentSectionElement: HTMLElement;
|
|
10
|
+
let currentBlockOutputElement: HTMLElement;
|
|
48
11
|
let scrollPosition = 0;
|
|
49
12
|
let outputElement: HTMLElement;
|
|
50
13
|
let settings: SquiffySettings;
|
|
51
14
|
let storageFallback: Record<string, string> = {};
|
|
15
|
+
let textProcessor: TextProcessor;
|
|
16
|
+
const emitter = new Emitter<SquiffyEventMap>();
|
|
52
17
|
|
|
53
18
|
function set(attribute: string, value: any) {
|
|
54
19
|
if (typeof value === 'undefined') value = true;
|
|
@@ -73,68 +38,75 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
73
38
|
return JSON.parse(result);
|
|
74
39
|
}
|
|
75
40
|
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
41
|
+
function handleLink(link: HTMLElement): boolean {
|
|
42
|
+
const outputSection = link.closest('.squiffy-output-section');
|
|
43
|
+
if (outputSection !== currentSectionElement) return false;
|
|
44
|
+
|
|
45
|
+
if (link.classList.contains('disabled')) return false;
|
|
46
|
+
|
|
47
|
+
let passage = link.getAttribute('data-passage');
|
|
48
|
+
let section = link.getAttribute('data-section');
|
|
49
|
+
const rotateAttr = link.getAttribute('data-rotate');
|
|
50
|
+
const sequenceAttr = link.getAttribute('data-sequence');
|
|
51
|
+
const rotateOrSequenceAttr = rotateAttr || sequenceAttr;
|
|
52
|
+
if (passage) {
|
|
53
|
+
disableLink(link);
|
|
54
|
+
set('_turncount', get('_turncount') + 1);
|
|
55
|
+
passage = processLink(passage);
|
|
84
56
|
if (passage) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
57
|
+
newBlockOutputElement();
|
|
58
|
+
showPassage(passage);
|
|
59
|
+
}
|
|
60
|
+
const turnPassage = '@' + get('_turncount');
|
|
61
|
+
if (currentSection.passages) {
|
|
62
|
+
if (turnPassage in currentSection.passages) {
|
|
63
|
+
showPassage(turnPassage);
|
|
91
64
|
}
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
}
|
|
65
|
+
if ('@last' in currentSection.passages && get('_turncount') >= (currentSection.passageCount || 0)) {
|
|
66
|
+
showPassage('@last');
|
|
100
67
|
}
|
|
101
68
|
}
|
|
102
|
-
|
|
103
|
-
|
|
69
|
+
|
|
70
|
+
emitter.emit('linkClick', { linkType: 'passage' });
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (section) {
|
|
75
|
+
section = processLink(section);
|
|
76
|
+
if (section) {
|
|
77
|
+
go(section);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
emitter.emit('linkClick', { linkType: 'section' });
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (rotateOrSequenceAttr) {
|
|
85
|
+
const result = rotate(rotateOrSequenceAttr, rotateAttr ? link.innerText : '');
|
|
86
|
+
link.innerHTML = result[0]!.replace(/"/g, '"').replace(/'/g, '\'');
|
|
87
|
+
const dataAttribute = rotateAttr ? 'data-rotate' : 'data-sequence';
|
|
88
|
+
link.setAttribute(dataAttribute, result[1] || '');
|
|
89
|
+
if (!result[1]) {
|
|
104
90
|
disableLink(link);
|
|
105
|
-
section = processLink(section);
|
|
106
|
-
if (section) {
|
|
107
|
-
go(section);
|
|
108
|
-
}
|
|
109
91
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const dataAttribute = rotateAttr ? 'data-rotate' : 'data-sequence';
|
|
114
|
-
link.setAttribute(dataAttribute, result[1] || '');
|
|
115
|
-
if (!result[1]) {
|
|
116
|
-
disableLink(link);
|
|
117
|
-
}
|
|
118
|
-
const attribute = link.getAttribute('data-attribute');
|
|
119
|
-
if (attribute) {
|
|
120
|
-
set(attribute, result[0]);
|
|
121
|
-
}
|
|
122
|
-
save();
|
|
92
|
+
const attribute = link.getAttribute('data-attribute');
|
|
93
|
+
if (attribute) {
|
|
94
|
+
set(attribute, result[0]);
|
|
123
95
|
}
|
|
96
|
+
save();
|
|
97
|
+
|
|
98
|
+
emitter.emit('linkClick', { linkType: rotateAttr ? 'rotate' : 'sequence' });
|
|
99
|
+
return true;
|
|
124
100
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
101
|
+
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function handleClick(event: Event) {
|
|
106
|
+
const target = event.target as HTMLElement;
|
|
107
|
+
if (target.classList.contains('squiffy-link')) {
|
|
108
|
+
handleLink(target);
|
|
131
109
|
}
|
|
132
|
-
|
|
133
|
-
document.addEventListener('click', handleClick);
|
|
134
|
-
document.addEventListener('keypress', function (event) {
|
|
135
|
-
if (event.key !== "Enter") return;
|
|
136
|
-
handleClick(event);
|
|
137
|
-
});
|
|
138
110
|
}
|
|
139
111
|
|
|
140
112
|
function disableLink(link: Element) {
|
|
@@ -142,10 +114,6 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
142
114
|
link.setAttribute('tabindex', '-1');
|
|
143
115
|
}
|
|
144
116
|
|
|
145
|
-
function disableLinks(links: NodeListOf<Element>) {
|
|
146
|
-
links.forEach(disableLink);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
117
|
function begin() {
|
|
150
118
|
if (!load()) {
|
|
151
119
|
go(story.start);
|
|
@@ -260,23 +228,23 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
260
228
|
labelElement.classList.add('fade-out');
|
|
261
229
|
}
|
|
262
230
|
|
|
263
|
-
function go(
|
|
231
|
+
function go(sectionName: string) {
|
|
264
232
|
set('_transition', null);
|
|
265
|
-
newSection();
|
|
266
|
-
currentSection = story.sections[
|
|
233
|
+
newSection(sectionName);
|
|
234
|
+
currentSection = story.sections[sectionName];
|
|
267
235
|
if (!currentSection) return;
|
|
268
|
-
set('_section',
|
|
269
|
-
setSeen(
|
|
236
|
+
set('_section', sectionName);
|
|
237
|
+
setSeen(sectionName);
|
|
270
238
|
const master = story.sections[''];
|
|
271
239
|
if (master) {
|
|
272
240
|
run(master);
|
|
273
|
-
ui.write(master.text || '');
|
|
241
|
+
ui.write(master.text || '', "[[]]");
|
|
274
242
|
}
|
|
275
243
|
run(currentSection);
|
|
276
244
|
// The JS might have changed which section we're in
|
|
277
|
-
if (get('_section') ==
|
|
245
|
+
if (get('_section') == sectionName) {
|
|
278
246
|
set('_turncount', 0);
|
|
279
|
-
ui.write(currentSection.text || '');
|
|
247
|
+
ui.write(currentSection.text || '', `[[${sectionName}]]`);
|
|
280
248
|
save();
|
|
281
249
|
}
|
|
282
250
|
}
|
|
@@ -289,7 +257,17 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
289
257
|
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
258
|
}
|
|
291
259
|
if (section.jsIndex !== undefined) {
|
|
292
|
-
|
|
260
|
+
const squiffy = {
|
|
261
|
+
get: get,
|
|
262
|
+
set: set,
|
|
263
|
+
ui: {
|
|
264
|
+
transition: ui.transition,
|
|
265
|
+
},
|
|
266
|
+
story: {
|
|
267
|
+
go: go,
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
story.js[section.jsIndex](squiffy, get, set);
|
|
293
271
|
}
|
|
294
272
|
}
|
|
295
273
|
|
|
@@ -297,22 +275,24 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
297
275
|
let passage = currentSection.passages && currentSection.passages[passageName];
|
|
298
276
|
const masterSection = story.sections[''];
|
|
299
277
|
if (!passage && masterSection && masterSection.passages) passage = masterSection.passages[passageName];
|
|
300
|
-
if (!passage)
|
|
278
|
+
if (!passage) {
|
|
279
|
+
throw `No passage named ${passageName} in the current section or master section`;
|
|
280
|
+
}
|
|
301
281
|
setSeen(passageName);
|
|
302
282
|
if (masterSection && masterSection.passages) {
|
|
303
283
|
const masterPassage = masterSection.passages[''];
|
|
304
284
|
if (masterPassage) {
|
|
305
285
|
run(masterPassage);
|
|
306
|
-
ui.write(masterPassage.text || '');
|
|
286
|
+
ui.write(masterPassage.text || '', `[[]][]`);
|
|
307
287
|
}
|
|
308
288
|
}
|
|
309
289
|
const master = currentSection.passages && currentSection.passages[''];
|
|
310
290
|
if (master) {
|
|
311
291
|
run(master);
|
|
312
|
-
ui.write(master.text || '');
|
|
292
|
+
ui.write(master.text || '', `[[${get("_section")}]][]`);
|
|
313
293
|
}
|
|
314
294
|
run(passage);
|
|
315
|
-
ui.write(passage.text || '');
|
|
295
|
+
ui.write(passage.text || '', `[[${get("_section")}]][${passageName}]`);
|
|
316
296
|
save();
|
|
317
297
|
}
|
|
318
298
|
|
|
@@ -356,9 +336,10 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
356
336
|
const output = get('_output');
|
|
357
337
|
if (!output) return false;
|
|
358
338
|
outputElement.innerHTML = output;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
339
|
+
|
|
340
|
+
currentSectionElement = outputElement.querySelector('.squiffy-output-section:last-child');
|
|
341
|
+
currentBlockOutputElement = outputElement.querySelector('.squiffy-output-block:last-child');
|
|
342
|
+
|
|
362
343
|
currentSection = story.sections[get('_section')];
|
|
363
344
|
const transition = get('_transition');
|
|
364
345
|
if (transition) {
|
|
@@ -381,10 +362,15 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
381
362
|
if (!seenSections) return false;
|
|
382
363
|
return (seenSections.indexOf(sectionName) > -1);
|
|
383
364
|
}
|
|
365
|
+
|
|
366
|
+
function newBlockOutputElement() {
|
|
367
|
+
currentBlockOutputElement = document.createElement('div');
|
|
368
|
+
currentBlockOutputElement.classList.add('squiffy-output-block');
|
|
369
|
+
currentSectionElement?.appendChild(currentBlockOutputElement);
|
|
370
|
+
}
|
|
384
371
|
|
|
385
|
-
function newSection() {
|
|
372
|
+
function newSection(sectionName: string | null) {
|
|
386
373
|
if (currentSectionElement) {
|
|
387
|
-
disableLinks(currentSectionElement.querySelectorAll('.squiffy-link'));
|
|
388
374
|
currentSectionElement.querySelectorAll('input').forEach(el => {
|
|
389
375
|
const attribute = el.getAttribute('data-attribute') || el.id;
|
|
390
376
|
if (attribute) set(attribute, el.value);
|
|
@@ -409,26 +395,32 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
409
395
|
const id = 'squiffy-section-' + sectionCount;
|
|
410
396
|
|
|
411
397
|
currentSectionElement = document.createElement('div');
|
|
398
|
+
currentSectionElement.classList.add('squiffy-output-section');
|
|
412
399
|
currentSectionElement.id = id;
|
|
400
|
+
if (sectionName) {
|
|
401
|
+
currentSectionElement.setAttribute('data-section', `${sectionName}`);
|
|
402
|
+
}
|
|
413
403
|
outputElement.appendChild(currentSectionElement);
|
|
414
|
-
|
|
415
|
-
set('_output-section', id);
|
|
404
|
+
newBlockOutputElement();
|
|
416
405
|
}
|
|
417
406
|
|
|
418
407
|
const ui = {
|
|
419
|
-
write: (text: string) => {
|
|
420
|
-
if (!
|
|
408
|
+
write: (text: string, source: string) => {
|
|
409
|
+
if (!currentBlockOutputElement) return;
|
|
421
410
|
scrollPosition = outputElement.scrollHeight;
|
|
422
411
|
|
|
423
412
|
const div = document.createElement('div');
|
|
424
|
-
|
|
413
|
+
if (source) {
|
|
414
|
+
div.setAttribute('data-source', source);
|
|
415
|
+
}
|
|
416
|
+
currentBlockOutputElement.appendChild(div);
|
|
425
417
|
div.innerHTML = ui.processText(text);
|
|
426
418
|
|
|
427
419
|
ui.scrollToEnd();
|
|
428
420
|
},
|
|
429
421
|
clearScreen: () => {
|
|
430
422
|
outputElement.innerHTML = '';
|
|
431
|
-
newSection();
|
|
423
|
+
newSection(null);
|
|
432
424
|
},
|
|
433
425
|
scrollToEnd: () => {
|
|
434
426
|
if (settings.scroll === 'element') {
|
|
@@ -449,196 +441,91 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
449
441
|
}
|
|
450
442
|
},
|
|
451
443
|
processText: (text: string) => {
|
|
452
|
-
function process(text: string, data: any) {
|
|
453
|
-
let containsUnprocessedSection = false;
|
|
454
|
-
const open = text.indexOf('{');
|
|
455
|
-
let close;
|
|
456
|
-
|
|
457
|
-
if (open > -1) {
|
|
458
|
-
let nestCount = 1;
|
|
459
|
-
let searchStart = open + 1;
|
|
460
|
-
let finished = false;
|
|
461
|
-
|
|
462
|
-
while (!finished) {
|
|
463
|
-
const nextOpen = text.indexOf('{', searchStart);
|
|
464
|
-
const nextClose = text.indexOf('}', searchStart);
|
|
465
|
-
|
|
466
|
-
if (nextClose > -1) {
|
|
467
|
-
if (nextOpen > -1 && nextOpen < nextClose) {
|
|
468
|
-
nestCount++;
|
|
469
|
-
searchStart = nextOpen + 1;
|
|
470
|
-
}
|
|
471
|
-
else {
|
|
472
|
-
nestCount--;
|
|
473
|
-
searchStart = nextClose + 1;
|
|
474
|
-
if (nestCount === 0) {
|
|
475
|
-
close = nextClose;
|
|
476
|
-
containsUnprocessedSection = true;
|
|
477
|
-
finished = true;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
else {
|
|
482
|
-
finished = true;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (containsUnprocessedSection) {
|
|
488
|
-
const section = text.substring(open + 1, close);
|
|
489
|
-
const value = processTextCommand(section, data);
|
|
490
|
-
text = text.substring(0, open) + value + process(text.substring(close! + 1), data);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
return (text);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function processTextCommand(text: string, data: any) {
|
|
497
|
-
if (startsWith(text, 'if ')) {
|
|
498
|
-
return processTextCommand_If(text, data);
|
|
499
|
-
}
|
|
500
|
-
else if (startsWith(text, 'else:')) {
|
|
501
|
-
return processTextCommand_Else(text, data);
|
|
502
|
-
}
|
|
503
|
-
else if (startsWith(text, 'label:')) {
|
|
504
|
-
return processTextCommand_Label(text, data);
|
|
505
|
-
}
|
|
506
|
-
else if (/^rotate[: ]/.test(text)) {
|
|
507
|
-
return processTextCommand_Rotate('rotate', text);
|
|
508
|
-
}
|
|
509
|
-
else if (/^sequence[: ]/.test(text)) {
|
|
510
|
-
return processTextCommand_Rotate('sequence', text);
|
|
511
|
-
}
|
|
512
|
-
else if (currentSection.passages && text in currentSection.passages) {
|
|
513
|
-
return process(currentSection.passages[text].text || '', data);
|
|
514
|
-
}
|
|
515
|
-
else if (text in story.sections) {
|
|
516
|
-
return process(story.sections[text].text || '', data);
|
|
517
|
-
}
|
|
518
|
-
else if (startsWith(text, '@') && !startsWith(text, '@replace')) {
|
|
519
|
-
processAttributes(text.substring(1).split(","));
|
|
520
|
-
return "";
|
|
521
|
-
}
|
|
522
|
-
return get(text);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function processTextCommand_If(section: string, data: any) {
|
|
526
|
-
const command = section.substring(3);
|
|
527
|
-
const colon = command.indexOf(':');
|
|
528
|
-
if (colon == -1) {
|
|
529
|
-
return ('{if ' + command + '}');
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const text = command.substring(colon + 1);
|
|
533
|
-
let condition = command.substring(0, colon);
|
|
534
|
-
condition = condition.replace("<", "<");
|
|
535
|
-
const operatorRegex = /([\w ]*)(=|<=|>=|<>|<|>)(.*)/;
|
|
536
|
-
const match = operatorRegex.exec(condition);
|
|
537
|
-
|
|
538
|
-
let result = false;
|
|
539
|
-
|
|
540
|
-
if (match) {
|
|
541
|
-
const lhs = get(match[1]);
|
|
542
|
-
const op = match[2];
|
|
543
|
-
let rhs = match[3];
|
|
544
|
-
|
|
545
|
-
if (startsWith(rhs, '@')) rhs = get(rhs.substring(1));
|
|
546
|
-
|
|
547
|
-
if (op == '=' && lhs == rhs) result = true;
|
|
548
|
-
if (op == '<>' && lhs != rhs) result = true;
|
|
549
|
-
if (op == '>' && lhs > rhs) result = true;
|
|
550
|
-
if (op == '<' && lhs < rhs) result = true;
|
|
551
|
-
if (op == '>=' && lhs >= rhs) result = true;
|
|
552
|
-
if (op == '<=' && lhs <= rhs) result = true;
|
|
553
|
-
}
|
|
554
|
-
else {
|
|
555
|
-
let checkValue = true;
|
|
556
|
-
if (startsWith(condition, 'not ')) {
|
|
557
|
-
condition = condition.substring(4);
|
|
558
|
-
checkValue = false;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (startsWith(condition, 'seen ')) {
|
|
562
|
-
result = (seen(condition.substring(5)) == checkValue);
|
|
563
|
-
}
|
|
564
|
-
else {
|
|
565
|
-
let value = get(condition);
|
|
566
|
-
if (value === null) value = false;
|
|
567
|
-
result = (value == checkValue);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const textResult = result ? process(text, data) : '';
|
|
572
|
-
|
|
573
|
-
data.lastIf = result;
|
|
574
|
-
return textResult;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function processTextCommand_Else(section: string, data: any) {
|
|
578
|
-
if (!('lastIf' in data) || data.lastIf) return '';
|
|
579
|
-
const text = section.substring(5);
|
|
580
|
-
return process(text, data);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function processTextCommand_Label(section: string, data: any) {
|
|
584
|
-
const command = section.substring(6);
|
|
585
|
-
const eq = command.indexOf('=');
|
|
586
|
-
if (eq == -1) {
|
|
587
|
-
return ('{label:' + command + '}');
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const text = command.substring(eq + 1);
|
|
591
|
-
const label = command.substring(0, eq);
|
|
592
|
-
|
|
593
|
-
return '<span class="squiffy-label-' + label + '">' + process(text, data) + '</span>';
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function processTextCommand_Rotate(type: string, section: string) {
|
|
597
|
-
let options;
|
|
598
|
-
let attribute = '';
|
|
599
|
-
if (section.substring(type.length, type.length + 1) == ' ') {
|
|
600
|
-
const colon = section.indexOf(':');
|
|
601
|
-
if (colon == -1) {
|
|
602
|
-
return '{' + section + '}';
|
|
603
|
-
}
|
|
604
|
-
options = section.substring(colon + 1);
|
|
605
|
-
attribute = section.substring(type.length + 1, colon);
|
|
606
|
-
}
|
|
607
|
-
else {
|
|
608
|
-
options = section.substring(type.length + 1);
|
|
609
|
-
}
|
|
610
|
-
// TODO: Check - previously there was no second parameter here
|
|
611
|
-
const rotation = rotate(options.replace(/"/g, '"').replace(/'/g, '''), null);
|
|
612
|
-
if (attribute) {
|
|
613
|
-
set(attribute, rotation[0]);
|
|
614
|
-
}
|
|
615
|
-
return '<a class="squiffy-link" data-' + type + '="' + rotation[1] + '" data-attribute="' + attribute + '" role="link">' + rotation[0] + '</a>';
|
|
616
|
-
}
|
|
617
|
-
|
|
618
444
|
const data = {
|
|
619
445
|
fulltext: text
|
|
620
446
|
};
|
|
621
|
-
return process(text, data);
|
|
447
|
+
return textProcessor.process(text, data);
|
|
622
448
|
},
|
|
623
449
|
transition: function (f: any) {
|
|
624
450
|
set('_transition', f.toString());
|
|
625
451
|
f();
|
|
626
452
|
},
|
|
627
453
|
};
|
|
628
|
-
|
|
629
|
-
function
|
|
630
|
-
return
|
|
454
|
+
|
|
455
|
+
function safeQuerySelector(name: string) {
|
|
456
|
+
return name.replace(/'/g, "\\'");
|
|
631
457
|
}
|
|
632
|
-
|
|
633
|
-
function
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
458
|
+
|
|
459
|
+
function getSectionContent(section: string) {
|
|
460
|
+
return outputElement.querySelectorAll(`[data-source='[[${safeQuerySelector(section)}]]']`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function getPassageContent(section: string, passage: string) {
|
|
464
|
+
return outputElement.querySelectorAll(`[data-source='[[${safeQuerySelector(section)}]][${safeQuerySelector(passage)}]']`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function updateElementTextPreservingDisabledPassageLinks(element: Element, text: string) {
|
|
468
|
+
// Record which passage links are disabled
|
|
469
|
+
const disabledPassages = Array.from(element
|
|
470
|
+
.querySelectorAll("a.link-passage.disabled"))
|
|
471
|
+
.map((el: HTMLElement) => el.getAttribute("data-passage"));
|
|
472
|
+
|
|
473
|
+
element.innerHTML = text;
|
|
474
|
+
|
|
475
|
+
// Re-disable links that were disabled before the update
|
|
476
|
+
for (const passage of disabledPassages) {
|
|
477
|
+
const link = element.querySelector(`a.link-passage[data-passage="${passage}"]`);
|
|
478
|
+
if (link) disableLink(link);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function update(newStory: Story) {
|
|
483
|
+
for (const existingSection of Object.keys(story.sections)) {
|
|
484
|
+
const elements = getSectionContent(existingSection);
|
|
485
|
+
if (elements.length) {
|
|
486
|
+
const newSection = newStory.sections[existingSection];
|
|
487
|
+
if (!newSection) {
|
|
488
|
+
// section has been deleted
|
|
489
|
+
for (const element of elements) {
|
|
490
|
+
const parentOutputSection = element.closest('.squiffy-output-section');
|
|
491
|
+
parentOutputSection.remove();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else if (newSection.text && newSection.text != story.sections[existingSection].text) {
|
|
495
|
+
// section has been updated
|
|
496
|
+
for (const element of elements) {
|
|
497
|
+
updateElementTextPreservingDisabledPassageLinks(element, newSection.text);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (!story.sections[existingSection].passages) continue;
|
|
503
|
+
|
|
504
|
+
for (const existingPassage of Object.keys(story.sections[existingSection].passages)) {
|
|
505
|
+
const elements = getPassageContent(existingSection, existingPassage);
|
|
506
|
+
if (!elements.length) continue;
|
|
507
|
+
|
|
508
|
+
const newPassage = newStory.sections[existingSection]?.passages && newStory.sections[existingSection]?.passages[existingPassage];
|
|
509
|
+
if (!newPassage) {
|
|
510
|
+
// passage has been deleted
|
|
511
|
+
for (const element of elements) {
|
|
512
|
+
const parentOutputBlock = element.closest('.squiffy-output-block');
|
|
513
|
+
parentOutputBlock.remove();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else if (newPassage.text && newPassage.text != story.sections[existingSection].passages[existingPassage].text) {
|
|
517
|
+
// passage has been updated
|
|
518
|
+
for (const element of elements) {
|
|
519
|
+
updateElementTextPreservingDisabledPassageLinks(element, newPassage.text);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
story = newStory;
|
|
526
|
+
currentSectionElement = outputElement.querySelector('.squiffy-output-section:last-child');
|
|
527
|
+
const sectionName = currentSectionElement.getAttribute('data-section');
|
|
528
|
+
currentSection = story.sections[sectionName];
|
|
642
529
|
}
|
|
643
530
|
|
|
644
531
|
outputElement = options.element;
|
|
@@ -659,12 +546,24 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
659
546
|
outputElement.style.overflowY = 'auto';
|
|
660
547
|
}
|
|
661
548
|
|
|
662
|
-
|
|
549
|
+
outputElement.addEventListener('click', handleClick);
|
|
550
|
+
outputElement.addEventListener('keypress', function (event) {
|
|
551
|
+
if (event.key !== "Enter") return;
|
|
552
|
+
handleClick(event);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
textProcessor = new TextProcessor(get, set, story, ui, seen, processAttributes);
|
|
556
|
+
|
|
663
557
|
begin();
|
|
664
558
|
|
|
665
559
|
return {
|
|
666
560
|
restart: restart,
|
|
667
561
|
get: get,
|
|
668
562
|
set: set,
|
|
563
|
+
clickLink: handleLink,
|
|
564
|
+
update: update,
|
|
565
|
+
on: (e, h) => emitter.on(e, h),
|
|
566
|
+
off: (e, h) => emitter.off(e, h),
|
|
567
|
+
once: (e, h) => emitter.once(e, h),
|
|
669
568
|
};
|
|
670
569
|
};
|