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/dist/events.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type LinkType = 'section' | 'passage' | 'rotate' | 'sequence';
|
|
2
|
+
export type SquiffyEventMap = {
|
|
3
|
+
linkClick: {
|
|
4
|
+
linkType: LinkType;
|
|
5
|
+
};
|
|
6
|
+
};
|
|
7
|
+
export type SquiffyEventHandler<E extends keyof SquiffyEventMap> = (payload: SquiffyEventMap[E]) => void;
|
|
8
|
+
export declare class Emitter<Events extends Record<string, any>> {
|
|
9
|
+
private listeners;
|
|
10
|
+
on<E extends keyof Events>(event: E, handler: (p: Events[E]) => void): () => void;
|
|
11
|
+
off<E extends keyof Events>(event: E, handler: (p: Events[E]) => void): void;
|
|
12
|
+
once<E extends keyof Events>(event: E, handler: (p: Events[E]) => void): () => void;
|
|
13
|
+
emit<E extends keyof Events>(event: E, payload: Events[E]): void;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export class Emitter {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.listeners = new Map();
|
|
4
|
+
}
|
|
5
|
+
on(event, handler) {
|
|
6
|
+
if (!this.listeners.has(event))
|
|
7
|
+
this.listeners.set(event, new Set());
|
|
8
|
+
this.listeners.get(event).add(handler);
|
|
9
|
+
return () => this.off(event, handler);
|
|
10
|
+
}
|
|
11
|
+
off(event, handler) {
|
|
12
|
+
this.listeners.get(event)?.delete(handler);
|
|
13
|
+
}
|
|
14
|
+
once(event, handler) {
|
|
15
|
+
const off = this.on(event, (payload) => {
|
|
16
|
+
off();
|
|
17
|
+
handler(payload);
|
|
18
|
+
});
|
|
19
|
+
return off;
|
|
20
|
+
}
|
|
21
|
+
emit(event, payload) {
|
|
22
|
+
// Fire handlers asynchronously so the runtime isn't blocked by user code.
|
|
23
|
+
queueMicrotask(() => {
|
|
24
|
+
this.listeners.get(event)?.forEach(h => {
|
|
25
|
+
try {
|
|
26
|
+
h(payload);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
// Swallow so a bad handler doesn't break the game; optionally log.
|
|
30
|
+
console.error(`[Squiffy] handler for "${String(event)}" failed`, err);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { startsWith, rotate } from "./utils.js";
|
|
2
|
+
import { TextProcessor } from './textProcessor.js';
|
|
3
|
+
import { Emitter } from './events.js';
|
|
4
|
+
export const init = (options) => {
|
|
5
|
+
let story;
|
|
6
|
+
let currentSection;
|
|
7
|
+
let currentSectionElement;
|
|
8
|
+
let currentBlockOutputElement;
|
|
9
|
+
let scrollPosition = 0;
|
|
10
|
+
let outputElement;
|
|
11
|
+
let settings;
|
|
12
|
+
let storageFallback = {};
|
|
13
|
+
let textProcessor;
|
|
14
|
+
const emitter = new Emitter();
|
|
15
|
+
function set(attribute, value) {
|
|
16
|
+
if (typeof value === 'undefined')
|
|
17
|
+
value = true;
|
|
18
|
+
if (settings.persist && window.localStorage) {
|
|
19
|
+
localStorage[story.id + '-' + attribute] = JSON.stringify(value);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
storageFallback[attribute] = JSON.stringify(value);
|
|
23
|
+
}
|
|
24
|
+
settings.onSet(attribute, value);
|
|
25
|
+
}
|
|
26
|
+
function get(attribute) {
|
|
27
|
+
let result;
|
|
28
|
+
if (settings.persist && window.localStorage) {
|
|
29
|
+
result = localStorage[story.id + '-' + attribute];
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
result = storageFallback[attribute];
|
|
33
|
+
}
|
|
34
|
+
if (!result)
|
|
35
|
+
return null;
|
|
36
|
+
return JSON.parse(result);
|
|
37
|
+
}
|
|
38
|
+
function handleLink(link) {
|
|
39
|
+
const outputSection = link.closest('.squiffy-output-section');
|
|
40
|
+
if (outputSection !== currentSectionElement)
|
|
41
|
+
return false;
|
|
42
|
+
if (link.classList.contains('disabled'))
|
|
43
|
+
return false;
|
|
44
|
+
let passage = link.getAttribute('data-passage');
|
|
45
|
+
let section = link.getAttribute('data-section');
|
|
46
|
+
const rotateAttr = link.getAttribute('data-rotate');
|
|
47
|
+
const sequenceAttr = link.getAttribute('data-sequence');
|
|
48
|
+
const rotateOrSequenceAttr = rotateAttr || sequenceAttr;
|
|
49
|
+
if (passage) {
|
|
50
|
+
disableLink(link);
|
|
51
|
+
set('_turncount', get('_turncount') + 1);
|
|
52
|
+
passage = processLink(passage);
|
|
53
|
+
if (passage) {
|
|
54
|
+
newBlockOutputElement();
|
|
55
|
+
showPassage(passage);
|
|
56
|
+
}
|
|
57
|
+
const turnPassage = '@' + get('_turncount');
|
|
58
|
+
if (currentSection.passages) {
|
|
59
|
+
if (turnPassage in currentSection.passages) {
|
|
60
|
+
showPassage(turnPassage);
|
|
61
|
+
}
|
|
62
|
+
if ('@last' in currentSection.passages && get('_turncount') >= (currentSection.passageCount || 0)) {
|
|
63
|
+
showPassage('@last');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
emitter.emit('linkClick', { linkType: 'passage' });
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
if (section) {
|
|
70
|
+
section = processLink(section);
|
|
71
|
+
if (section) {
|
|
72
|
+
go(section);
|
|
73
|
+
}
|
|
74
|
+
emitter.emit('linkClick', { linkType: 'section' });
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (rotateOrSequenceAttr) {
|
|
78
|
+
const result = rotate(rotateOrSequenceAttr, rotateAttr ? link.innerText : '');
|
|
79
|
+
link.innerHTML = result[0].replace(/"/g, '"').replace(/'/g, '\'');
|
|
80
|
+
const dataAttribute = rotateAttr ? 'data-rotate' : 'data-sequence';
|
|
81
|
+
link.setAttribute(dataAttribute, result[1] || '');
|
|
82
|
+
if (!result[1]) {
|
|
83
|
+
disableLink(link);
|
|
84
|
+
}
|
|
85
|
+
const attribute = link.getAttribute('data-attribute');
|
|
86
|
+
if (attribute) {
|
|
87
|
+
set(attribute, result[0]);
|
|
88
|
+
}
|
|
89
|
+
save();
|
|
90
|
+
emitter.emit('linkClick', { linkType: rotateAttr ? 'rotate' : 'sequence' });
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
function handleClick(event) {
|
|
96
|
+
const target = event.target;
|
|
97
|
+
if (target.classList.contains('squiffy-link')) {
|
|
98
|
+
handleLink(target);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function disableLink(link) {
|
|
102
|
+
link.classList.add('disabled');
|
|
103
|
+
link.setAttribute('tabindex', '-1');
|
|
104
|
+
}
|
|
105
|
+
function begin() {
|
|
106
|
+
if (!load()) {
|
|
107
|
+
go(story.start);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function processLink(link) {
|
|
111
|
+
const sections = link.split(',');
|
|
112
|
+
let first = true;
|
|
113
|
+
let target = null;
|
|
114
|
+
sections.forEach(function (section) {
|
|
115
|
+
section = section.trim();
|
|
116
|
+
if (startsWith(section, '@replace ')) {
|
|
117
|
+
replaceLabel(section.substring(9));
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
if (first) {
|
|
121
|
+
target = section;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
setAttribute(section);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
first = false;
|
|
128
|
+
});
|
|
129
|
+
return target;
|
|
130
|
+
}
|
|
131
|
+
function setAttribute(expr) {
|
|
132
|
+
expr = expr.replace(/^(\w*\s*):=(.*)$/, (_, name, value) => (name + "=" + ui.processText(value)));
|
|
133
|
+
const setRegex = /^([\w]*)\s*=\s*(.*)$/;
|
|
134
|
+
const setMatch = setRegex.exec(expr);
|
|
135
|
+
if (setMatch) {
|
|
136
|
+
const lhs = setMatch[1];
|
|
137
|
+
let rhs = setMatch[2];
|
|
138
|
+
if (isNaN(rhs)) {
|
|
139
|
+
if (startsWith(rhs, "@"))
|
|
140
|
+
rhs = get(rhs.substring(1));
|
|
141
|
+
set(lhs, rhs);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
set(lhs, parseFloat(rhs));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const incDecRegex = /^([\w]*)\s*([\+\-\*\/])=\s*(.*)$/;
|
|
149
|
+
const incDecMatch = incDecRegex.exec(expr);
|
|
150
|
+
if (incDecMatch) {
|
|
151
|
+
const lhs = incDecMatch[1];
|
|
152
|
+
const op = incDecMatch[2];
|
|
153
|
+
let rhs = incDecMatch[3];
|
|
154
|
+
if (startsWith(rhs, "@"))
|
|
155
|
+
rhs = get(rhs.substring(1));
|
|
156
|
+
const rhsNumeric = parseFloat(rhs);
|
|
157
|
+
let value = get(lhs);
|
|
158
|
+
if (value === null)
|
|
159
|
+
value = 0;
|
|
160
|
+
if (op == '+') {
|
|
161
|
+
value += rhsNumeric;
|
|
162
|
+
}
|
|
163
|
+
if (op == '-') {
|
|
164
|
+
value -= rhsNumeric;
|
|
165
|
+
}
|
|
166
|
+
if (op == '*') {
|
|
167
|
+
value *= rhsNumeric;
|
|
168
|
+
}
|
|
169
|
+
if (op == '/') {
|
|
170
|
+
value /= rhsNumeric;
|
|
171
|
+
}
|
|
172
|
+
set(lhs, value);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
let value = true;
|
|
176
|
+
if (startsWith(expr, 'not ')) {
|
|
177
|
+
expr = expr.substring(4);
|
|
178
|
+
value = false;
|
|
179
|
+
}
|
|
180
|
+
set(expr, value);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function replaceLabel(expr) {
|
|
185
|
+
const regex = /^([\w]*)\s*=\s*(.*)$/;
|
|
186
|
+
const match = regex.exec(expr);
|
|
187
|
+
if (!match)
|
|
188
|
+
return;
|
|
189
|
+
const label = match[1];
|
|
190
|
+
let text = match[2];
|
|
191
|
+
if (currentSection.passages && text in currentSection.passages) {
|
|
192
|
+
text = currentSection.passages[text].text || '';
|
|
193
|
+
}
|
|
194
|
+
else if (text in story.sections) {
|
|
195
|
+
text = story.sections[text].text || '';
|
|
196
|
+
}
|
|
197
|
+
const stripParags = /^<p>(.*)<\/p>$/;
|
|
198
|
+
const stripParagsMatch = stripParags.exec(text);
|
|
199
|
+
if (stripParagsMatch) {
|
|
200
|
+
text = stripParagsMatch[1];
|
|
201
|
+
}
|
|
202
|
+
const labelElement = outputElement.querySelector('.squiffy-label-' + label);
|
|
203
|
+
if (!labelElement)
|
|
204
|
+
return;
|
|
205
|
+
labelElement.addEventListener('transitionend', function () {
|
|
206
|
+
labelElement.innerHTML = ui.processText(text);
|
|
207
|
+
labelElement.addEventListener('transitionend', function () {
|
|
208
|
+
save();
|
|
209
|
+
}, { once: true });
|
|
210
|
+
labelElement.classList.remove('fade-out');
|
|
211
|
+
labelElement.classList.add('fade-in');
|
|
212
|
+
}, { once: true });
|
|
213
|
+
labelElement.classList.add('fade-out');
|
|
214
|
+
}
|
|
215
|
+
function go(sectionName) {
|
|
216
|
+
set('_transition', null);
|
|
217
|
+
newSection(sectionName);
|
|
218
|
+
currentSection = story.sections[sectionName];
|
|
219
|
+
if (!currentSection)
|
|
220
|
+
return;
|
|
221
|
+
set('_section', sectionName);
|
|
222
|
+
setSeen(sectionName);
|
|
223
|
+
const master = story.sections[''];
|
|
224
|
+
if (master) {
|
|
225
|
+
run(master);
|
|
226
|
+
ui.write(master.text || '', "[[]]");
|
|
227
|
+
}
|
|
228
|
+
run(currentSection);
|
|
229
|
+
// The JS might have changed which section we're in
|
|
230
|
+
if (get('_section') == sectionName) {
|
|
231
|
+
set('_turncount', 0);
|
|
232
|
+
ui.write(currentSection.text || '', `[[${sectionName}]]`);
|
|
233
|
+
save();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function run(section) {
|
|
237
|
+
if (section.clear) {
|
|
238
|
+
ui.clearScreen();
|
|
239
|
+
}
|
|
240
|
+
if (section.attributes) {
|
|
241
|
+
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)));
|
|
242
|
+
}
|
|
243
|
+
if (section.jsIndex !== undefined) {
|
|
244
|
+
const squiffy = {
|
|
245
|
+
get: get,
|
|
246
|
+
set: set,
|
|
247
|
+
ui: {
|
|
248
|
+
transition: ui.transition,
|
|
249
|
+
},
|
|
250
|
+
story: {
|
|
251
|
+
go: go,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
story.js[section.jsIndex](squiffy, get, set);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function showPassage(passageName) {
|
|
258
|
+
let passage = currentSection.passages && currentSection.passages[passageName];
|
|
259
|
+
const masterSection = story.sections[''];
|
|
260
|
+
if (!passage && masterSection && masterSection.passages)
|
|
261
|
+
passage = masterSection.passages[passageName];
|
|
262
|
+
if (!passage) {
|
|
263
|
+
throw `No passage named ${passageName} in the current section or master section`;
|
|
264
|
+
}
|
|
265
|
+
setSeen(passageName);
|
|
266
|
+
if (masterSection && masterSection.passages) {
|
|
267
|
+
const masterPassage = masterSection.passages[''];
|
|
268
|
+
if (masterPassage) {
|
|
269
|
+
run(masterPassage);
|
|
270
|
+
ui.write(masterPassage.text || '', `[[]][]`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const master = currentSection.passages && currentSection.passages[''];
|
|
274
|
+
if (master) {
|
|
275
|
+
run(master);
|
|
276
|
+
ui.write(master.text || '', `[[${get("_section")}]][]`);
|
|
277
|
+
}
|
|
278
|
+
run(passage);
|
|
279
|
+
ui.write(passage.text || '', `[[${get("_section")}]][${passageName}]`);
|
|
280
|
+
save();
|
|
281
|
+
}
|
|
282
|
+
function processAttributes(attributes) {
|
|
283
|
+
attributes.forEach(function (attribute) {
|
|
284
|
+
if (startsWith(attribute, '@replace ')) {
|
|
285
|
+
replaceLabel(attribute.substring(9));
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
setAttribute(attribute);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
function restart() {
|
|
293
|
+
if (settings.persist && window.localStorage && story.id) {
|
|
294
|
+
const keys = Object.keys(localStorage);
|
|
295
|
+
for (const key of keys) {
|
|
296
|
+
if (startsWith(key, story.id)) {
|
|
297
|
+
localStorage.removeItem(key);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
storageFallback = {};
|
|
303
|
+
}
|
|
304
|
+
if (settings.scroll === 'element') {
|
|
305
|
+
outputElement.innerHTML = '';
|
|
306
|
+
begin();
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
location.reload();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function save() {
|
|
313
|
+
set('_output', outputElement.innerHTML);
|
|
314
|
+
}
|
|
315
|
+
function load() {
|
|
316
|
+
const output = get('_output');
|
|
317
|
+
if (!output)
|
|
318
|
+
return false;
|
|
319
|
+
outputElement.innerHTML = output;
|
|
320
|
+
currentSectionElement = outputElement.querySelector('.squiffy-output-section:last-child');
|
|
321
|
+
currentBlockOutputElement = outputElement.querySelector('.squiffy-output-block:last-child');
|
|
322
|
+
currentSection = story.sections[get('_section')];
|
|
323
|
+
const transition = get('_transition');
|
|
324
|
+
if (transition) {
|
|
325
|
+
eval('(' + transition + ')()');
|
|
326
|
+
}
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
function setSeen(sectionName) {
|
|
330
|
+
let seenSections = get('_seen_sections');
|
|
331
|
+
if (!seenSections)
|
|
332
|
+
seenSections = [];
|
|
333
|
+
if (seenSections.indexOf(sectionName) == -1) {
|
|
334
|
+
seenSections.push(sectionName);
|
|
335
|
+
set('_seen_sections', seenSections);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function seen(sectionName) {
|
|
339
|
+
const seenSections = get('_seen_sections');
|
|
340
|
+
if (!seenSections)
|
|
341
|
+
return false;
|
|
342
|
+
return (seenSections.indexOf(sectionName) > -1);
|
|
343
|
+
}
|
|
344
|
+
function newBlockOutputElement() {
|
|
345
|
+
currentBlockOutputElement = document.createElement('div');
|
|
346
|
+
currentBlockOutputElement.classList.add('squiffy-output-block');
|
|
347
|
+
currentSectionElement?.appendChild(currentBlockOutputElement);
|
|
348
|
+
}
|
|
349
|
+
function newSection(sectionName) {
|
|
350
|
+
if (currentSectionElement) {
|
|
351
|
+
currentSectionElement.querySelectorAll('input').forEach(el => {
|
|
352
|
+
const attribute = el.getAttribute('data-attribute') || el.id;
|
|
353
|
+
if (attribute)
|
|
354
|
+
set(attribute, el.value);
|
|
355
|
+
el.disabled = true;
|
|
356
|
+
});
|
|
357
|
+
currentSectionElement.querySelectorAll("[contenteditable]").forEach(el => {
|
|
358
|
+
const attribute = el.getAttribute('data-attribute') || el.id;
|
|
359
|
+
if (attribute)
|
|
360
|
+
set(attribute, el.innerHTML);
|
|
361
|
+
el.contentEditable = 'false';
|
|
362
|
+
});
|
|
363
|
+
currentSectionElement.querySelectorAll('textarea').forEach(el => {
|
|
364
|
+
const attribute = el.getAttribute('data-attribute') || el.id;
|
|
365
|
+
if (attribute)
|
|
366
|
+
set(attribute, el.value);
|
|
367
|
+
el.disabled = true;
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const sectionCount = get('_section-count') + 1;
|
|
371
|
+
set('_section-count', sectionCount);
|
|
372
|
+
const id = 'squiffy-section-' + sectionCount;
|
|
373
|
+
currentSectionElement = document.createElement('div');
|
|
374
|
+
currentSectionElement.classList.add('squiffy-output-section');
|
|
375
|
+
currentSectionElement.id = id;
|
|
376
|
+
if (sectionName) {
|
|
377
|
+
currentSectionElement.setAttribute('data-section', `${sectionName}`);
|
|
378
|
+
}
|
|
379
|
+
outputElement.appendChild(currentSectionElement);
|
|
380
|
+
newBlockOutputElement();
|
|
381
|
+
}
|
|
382
|
+
const ui = {
|
|
383
|
+
write: (text, source) => {
|
|
384
|
+
if (!currentBlockOutputElement)
|
|
385
|
+
return;
|
|
386
|
+
scrollPosition = outputElement.scrollHeight;
|
|
387
|
+
const div = document.createElement('div');
|
|
388
|
+
if (source) {
|
|
389
|
+
div.setAttribute('data-source', source);
|
|
390
|
+
}
|
|
391
|
+
currentBlockOutputElement.appendChild(div);
|
|
392
|
+
div.innerHTML = ui.processText(text);
|
|
393
|
+
ui.scrollToEnd();
|
|
394
|
+
},
|
|
395
|
+
clearScreen: () => {
|
|
396
|
+
outputElement.innerHTML = '';
|
|
397
|
+
newSection(null);
|
|
398
|
+
},
|
|
399
|
+
scrollToEnd: () => {
|
|
400
|
+
if (settings.scroll === 'element') {
|
|
401
|
+
const scrollTo = outputElement.scrollHeight - outputElement.clientHeight;
|
|
402
|
+
const currentScrollTop = outputElement.scrollTop;
|
|
403
|
+
if (scrollTo > (currentScrollTop || 0)) {
|
|
404
|
+
outputElement.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
let scrollTo = scrollPosition;
|
|
409
|
+
const currentScrollTop = Math.max(document.body.scrollTop, document.documentElement.scrollTop);
|
|
410
|
+
if (scrollTo > currentScrollTop) {
|
|
411
|
+
const maxScrollTop = document.documentElement.scrollHeight - window.innerHeight;
|
|
412
|
+
if (scrollTo > maxScrollTop)
|
|
413
|
+
scrollTo = maxScrollTop;
|
|
414
|
+
window.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
processText: (text) => {
|
|
419
|
+
const data = {
|
|
420
|
+
fulltext: text
|
|
421
|
+
};
|
|
422
|
+
return textProcessor.process(text, data);
|
|
423
|
+
},
|
|
424
|
+
transition: function (f) {
|
|
425
|
+
set('_transition', f.toString());
|
|
426
|
+
f();
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
function safeQuerySelector(name) {
|
|
430
|
+
return name.replace(/'/g, "\\'");
|
|
431
|
+
}
|
|
432
|
+
function getSectionContent(section) {
|
|
433
|
+
return outputElement.querySelectorAll(`[data-source='[[${safeQuerySelector(section)}]]']`);
|
|
434
|
+
}
|
|
435
|
+
function getPassageContent(section, passage) {
|
|
436
|
+
return outputElement.querySelectorAll(`[data-source='[[${safeQuerySelector(section)}]][${safeQuerySelector(passage)}]']`);
|
|
437
|
+
}
|
|
438
|
+
function updateElementTextPreservingDisabledPassageLinks(element, text) {
|
|
439
|
+
// Record which passage links are disabled
|
|
440
|
+
const disabledPassages = Array.from(element
|
|
441
|
+
.querySelectorAll("a.link-passage.disabled"))
|
|
442
|
+
.map((el) => el.getAttribute("data-passage"));
|
|
443
|
+
element.innerHTML = text;
|
|
444
|
+
// Re-disable links that were disabled before the update
|
|
445
|
+
for (const passage of disabledPassages) {
|
|
446
|
+
const link = element.querySelector(`a.link-passage[data-passage="${passage}"]`);
|
|
447
|
+
if (link)
|
|
448
|
+
disableLink(link);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function update(newStory) {
|
|
452
|
+
for (const existingSection of Object.keys(story.sections)) {
|
|
453
|
+
const elements = getSectionContent(existingSection);
|
|
454
|
+
if (elements.length) {
|
|
455
|
+
const newSection = newStory.sections[existingSection];
|
|
456
|
+
if (!newSection) {
|
|
457
|
+
// section has been deleted
|
|
458
|
+
for (const element of elements) {
|
|
459
|
+
const parentOutputSection = element.closest('.squiffy-output-section');
|
|
460
|
+
parentOutputSection.remove();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
else if (newSection.text && newSection.text != story.sections[existingSection].text) {
|
|
464
|
+
// section has been updated
|
|
465
|
+
for (const element of elements) {
|
|
466
|
+
updateElementTextPreservingDisabledPassageLinks(element, newSection.text);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (!story.sections[existingSection].passages)
|
|
471
|
+
continue;
|
|
472
|
+
for (const existingPassage of Object.keys(story.sections[existingSection].passages)) {
|
|
473
|
+
const elements = getPassageContent(existingSection, existingPassage);
|
|
474
|
+
if (!elements.length)
|
|
475
|
+
continue;
|
|
476
|
+
const newPassage = newStory.sections[existingSection]?.passages && newStory.sections[existingSection]?.passages[existingPassage];
|
|
477
|
+
if (!newPassage) {
|
|
478
|
+
// passage has been deleted
|
|
479
|
+
for (const element of elements) {
|
|
480
|
+
const parentOutputBlock = element.closest('.squiffy-output-block');
|
|
481
|
+
parentOutputBlock.remove();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
else if (newPassage.text && newPassage.text != story.sections[existingSection].passages[existingPassage].text) {
|
|
485
|
+
// passage has been updated
|
|
486
|
+
for (const element of elements) {
|
|
487
|
+
updateElementTextPreservingDisabledPassageLinks(element, newPassage.text);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
story = newStory;
|
|
493
|
+
currentSectionElement = outputElement.querySelector('.squiffy-output-section:last-child');
|
|
494
|
+
const sectionName = currentSectionElement.getAttribute('data-section');
|
|
495
|
+
currentSection = story.sections[sectionName];
|
|
496
|
+
}
|
|
497
|
+
outputElement = options.element;
|
|
498
|
+
story = options.story;
|
|
499
|
+
settings = {
|
|
500
|
+
scroll: options.scroll || 'body',
|
|
501
|
+
persist: (options.persist === undefined) ? true : options.persist,
|
|
502
|
+
onSet: options.onSet || (() => { })
|
|
503
|
+
};
|
|
504
|
+
if (options.persist === true && !story.id) {
|
|
505
|
+
console.warn("Persist is set to true in Squiffy runtime options, but no story id has been set. Persist will be disabled.");
|
|
506
|
+
settings.persist = false;
|
|
507
|
+
}
|
|
508
|
+
if (settings.scroll === 'element') {
|
|
509
|
+
outputElement.style.overflowY = 'auto';
|
|
510
|
+
}
|
|
511
|
+
outputElement.addEventListener('click', handleClick);
|
|
512
|
+
outputElement.addEventListener('keypress', function (event) {
|
|
513
|
+
if (event.key !== "Enter")
|
|
514
|
+
return;
|
|
515
|
+
handleClick(event);
|
|
516
|
+
});
|
|
517
|
+
textProcessor = new TextProcessor(get, set, story, ui, seen, processAttributes);
|
|
518
|
+
begin();
|
|
519
|
+
return {
|
|
520
|
+
restart: restart,
|
|
521
|
+
get: get,
|
|
522
|
+
set: set,
|
|
523
|
+
clickLink: handleLink,
|
|
524
|
+
update: update,
|
|
525
|
+
on: (e, h) => emitter.on(e, h),
|
|
526
|
+
off: (e, h) => emitter.off(e, h),
|
|
527
|
+
once: (e, h) => emitter.once(e, h),
|
|
528
|
+
};
|
|
529
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|