squiffy-compiler 6.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/compiler.d.ts +51 -0
- package/dist/compiler.js +542 -0
- package/dist/compiler.test.d.ts +1 -0
- package/dist/compiler.test.js +73 -0
- package/dist/external-files.d.ts +5 -0
- package/dist/external-files.js +21 -0
- package/dist/index.template.html +39 -0
- package/dist/packager.d.ts +1 -0
- package/dist/packager.js +78 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +15 -0
- package/dist/squiffy.d.ts +1 -0
- package/dist/squiffy.js +29 -0
- package/dist/squiffy.runtime.d.ts +34 -0
- package/dist/squiffy.template.d.ts +29 -0
- package/dist/squiffy.template.js +598 -0
- package/dist/style.template.css +52 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/examples/attributes/attributes.squiffy +81 -0
- package/examples/clearscreen/clearscreen.squiffy +15 -0
- package/examples/continue/continue.squiffy +18 -0
- package/examples/helloworld/helloworld.squiffy +1 -0
- package/examples/import/file2.squiffy +8 -0
- package/examples/import/test.js +3 -0
- package/examples/import/test.squiffy +5 -0
- package/examples/input/input.squiffy +22 -0
- package/examples/last/last.squiffy +32 -0
- package/examples/master/master.squiffy +35 -0
- package/examples/replace/replace.squiffy +27 -0
- package/examples/rotate/rotate.squiffy +25 -0
- package/examples/sectiontrack/sectiontrack.squiffy +16 -0
- package/examples/start/start.squiffy +7 -0
- package/examples/test/example.squiffy +52 -0
- package/examples/textprocessor/textprocessor.squiffy +21 -0
- package/examples/transitions/transitions.squiffy +53 -0
- package/examples/turncount/turncount.squiffy +41 -0
- package/examples/warnings/warnings.squiffy +23 -0
- package/examples/warnings/warnings2.squiffy +3 -0
- package/package.json +46 -0
- package/src/__snapshots__/compiler.test.ts.snap +716 -0
- package/src/compiler.test.ts +86 -0
- package/src/compiler.ts +546 -0
- package/src/external-files.ts +22 -0
- package/src/index.template.html +39 -0
- package/src/packager.ts +97 -0
- package/src/server.ts +19 -0
- package/src/squiffy.runtime.ts +670 -0
- package/src/squiffy.ts +36 -0
- package/src/style.template.css +52 -0
- package/src/version.ts +1 -0
- package/tsconfig.json +22 -0
- package/tsconfig.runtime.json +12 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
interface SquiffyInitOptions {
|
|
2
|
+
element: HTMLElement;
|
|
3
|
+
story: Story;
|
|
4
|
+
scroll?: string,
|
|
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
|
+
}
|
|
43
|
+
|
|
44
|
+
export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
45
|
+
let story: Story;
|
|
46
|
+
let currentSection: Section;
|
|
47
|
+
let currentSectionElement: HTMLElement;
|
|
48
|
+
let scrollPosition = 0;
|
|
49
|
+
let outputElement: HTMLElement;
|
|
50
|
+
let settings: SquiffySettings;
|
|
51
|
+
let storageFallback: Record<string, string> = {};
|
|
52
|
+
|
|
53
|
+
function set(attribute: string, value: any) {
|
|
54
|
+
if (typeof value === 'undefined') value = true;
|
|
55
|
+
if (settings.persist && window.localStorage) {
|
|
56
|
+
localStorage[story.id + '-' + attribute] = JSON.stringify(value);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
storageFallback[attribute] = JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
settings.onSet(attribute, value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function get(attribute: string): any {
|
|
65
|
+
let result;
|
|
66
|
+
if (settings.persist && window.localStorage) {
|
|
67
|
+
result = localStorage[story.id + '-' + attribute];
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
result = storageFallback[attribute];
|
|
71
|
+
}
|
|
72
|
+
if (!result) return null;
|
|
73
|
+
return JSON.parse(result);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function initLinkHandler() {
|
|
77
|
+
function handleLink(link: HTMLElement) {
|
|
78
|
+
if (link.classList.contains('disabled')) return;
|
|
79
|
+
let passage = link.getAttribute('data-passage');
|
|
80
|
+
let section = link.getAttribute('data-section');
|
|
81
|
+
const rotateAttr = link.getAttribute('data-rotate');
|
|
82
|
+
const sequenceAttr = link.getAttribute('data-sequence');
|
|
83
|
+
const rotateOrSequenceAttr = rotateAttr || sequenceAttr;
|
|
84
|
+
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
|
+
currentSectionElement?.appendChild(document.createElement('hr'));
|
|
104
|
+
disableLink(link);
|
|
105
|
+
section = processLink(section);
|
|
106
|
+
if (section) {
|
|
107
|
+
go(section);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (rotateOrSequenceAttr) {
|
|
111
|
+
const result = rotate(rotateOrSequenceAttr, rotateAttr ? link.innerText : '');
|
|
112
|
+
link.innerHTML = result[0]!.replace(/"/g, '"').replace(/'/g, '\'');
|
|
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();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function handleClick(event: Event) {
|
|
127
|
+
const target = event.target as HTMLElement;
|
|
128
|
+
if (target.classList.contains('squiffy-link')) {
|
|
129
|
+
handleLink(target);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
document.addEventListener('click', handleClick);
|
|
134
|
+
document.addEventListener('keypress', function (event) {
|
|
135
|
+
if (event.key !== "Enter") return;
|
|
136
|
+
handleClick(event);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function disableLink(link: Element) {
|
|
141
|
+
link.classList.add('disabled');
|
|
142
|
+
link.setAttribute('tabindex', '-1');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function disableLinks(links: NodeListOf<Element>) {
|
|
146
|
+
links.forEach(disableLink);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function begin() {
|
|
150
|
+
if (!load()) {
|
|
151
|
+
go(story.start);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function processLink(link: string): string | null {
|
|
156
|
+
const sections = link.split(',');
|
|
157
|
+
let first = true;
|
|
158
|
+
let target = null;
|
|
159
|
+
sections.forEach(function (section) {
|
|
160
|
+
section = section.trim();
|
|
161
|
+
if (startsWith(section, '@replace ')) {
|
|
162
|
+
replaceLabel(section.substring(9));
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
if (first) {
|
|
166
|
+
target = section;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
setAttribute(section);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
first = false;
|
|
173
|
+
});
|
|
174
|
+
return target;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function setAttribute(expr: string) {
|
|
178
|
+
expr = expr.replace(/^(\w*\s*):=(.*)$/, (_, name, value) => (name + "=" + ui.processText(value)));
|
|
179
|
+
const setRegex = /^([\w]*)\s*=\s*(.*)$/;
|
|
180
|
+
const setMatch = setRegex.exec(expr);
|
|
181
|
+
if (setMatch) {
|
|
182
|
+
const lhs = setMatch[1];
|
|
183
|
+
let rhs = setMatch[2];
|
|
184
|
+
if (isNaN(rhs as any)) {
|
|
185
|
+
if (startsWith(rhs, "@")) rhs = get(rhs.substring(1));
|
|
186
|
+
set(lhs, rhs);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
set(lhs, parseFloat(rhs));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const incDecRegex = /^([\w]*)\s*([\+\-\*\/])=\s*(.*)$/;
|
|
194
|
+
const incDecMatch = incDecRegex.exec(expr);
|
|
195
|
+
if (incDecMatch) {
|
|
196
|
+
const lhs = incDecMatch[1];
|
|
197
|
+
const op = incDecMatch[2];
|
|
198
|
+
let rhs = incDecMatch[3];
|
|
199
|
+
if (startsWith(rhs, "@")) rhs = get(rhs.substring(1));
|
|
200
|
+
const rhsNumeric = parseFloat(rhs);
|
|
201
|
+
let value = get(lhs);
|
|
202
|
+
if (value === null) value = 0;
|
|
203
|
+
if (op == '+') {
|
|
204
|
+
value += rhsNumeric;
|
|
205
|
+
}
|
|
206
|
+
if (op == '-') {
|
|
207
|
+
value -= rhsNumeric;
|
|
208
|
+
}
|
|
209
|
+
if (op == '*') {
|
|
210
|
+
value *= rhsNumeric;
|
|
211
|
+
}
|
|
212
|
+
if (op == '/') {
|
|
213
|
+
value /= rhsNumeric;
|
|
214
|
+
}
|
|
215
|
+
set(lhs, value);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
let value = true;
|
|
219
|
+
if (startsWith(expr, 'not ')) {
|
|
220
|
+
expr = expr.substring(4);
|
|
221
|
+
value = false;
|
|
222
|
+
}
|
|
223
|
+
set(expr, value);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function replaceLabel(expr: string) {
|
|
229
|
+
const regex = /^([\w]*)\s*=\s*(.*)$/;
|
|
230
|
+
const match = regex.exec(expr);
|
|
231
|
+
if (!match) return;
|
|
232
|
+
const label = match[1];
|
|
233
|
+
let text = match[2];
|
|
234
|
+
if (currentSection.passages && text in currentSection.passages) {
|
|
235
|
+
text = currentSection.passages[text].text || '';
|
|
236
|
+
}
|
|
237
|
+
else if (text in story.sections) {
|
|
238
|
+
text = story.sections[text].text || '';
|
|
239
|
+
}
|
|
240
|
+
const stripParags = /^<p>(.*)<\/p>$/;
|
|
241
|
+
const stripParagsMatch = stripParags.exec(text);
|
|
242
|
+
if (stripParagsMatch) {
|
|
243
|
+
text = stripParagsMatch[1];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const labelElement = outputElement.querySelector('.squiffy-label-' + label);
|
|
247
|
+
if (!labelElement) return;
|
|
248
|
+
|
|
249
|
+
labelElement.addEventListener('transitionend', function () {
|
|
250
|
+
labelElement.innerHTML = ui.processText(text);
|
|
251
|
+
|
|
252
|
+
labelElement.addEventListener('transitionend', function () {
|
|
253
|
+
save();
|
|
254
|
+
}, { once: true });
|
|
255
|
+
|
|
256
|
+
labelElement.classList.remove('fade-out');
|
|
257
|
+
labelElement.classList.add('fade-in');
|
|
258
|
+
}, { once: true });
|
|
259
|
+
|
|
260
|
+
labelElement.classList.add('fade-out');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function go(section: string) {
|
|
264
|
+
set('_transition', null);
|
|
265
|
+
newSection();
|
|
266
|
+
currentSection = story.sections[section];
|
|
267
|
+
if (!currentSection) return;
|
|
268
|
+
set('_section', section);
|
|
269
|
+
setSeen(section);
|
|
270
|
+
const master = story.sections[''];
|
|
271
|
+
if (master) {
|
|
272
|
+
run(master);
|
|
273
|
+
ui.write(master.text || '');
|
|
274
|
+
}
|
|
275
|
+
run(currentSection);
|
|
276
|
+
// The JS might have changed which section we're in
|
|
277
|
+
if (get('_section') == section) {
|
|
278
|
+
set('_turncount', 0);
|
|
279
|
+
ui.write(currentSection.text || '');
|
|
280
|
+
save();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function run(section: Section) {
|
|
285
|
+
if (section.clear) {
|
|
286
|
+
ui.clearScreen();
|
|
287
|
+
}
|
|
288
|
+
if (section.attributes) {
|
|
289
|
+
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
|
+
}
|
|
291
|
+
if (section.jsIndex !== undefined) {
|
|
292
|
+
story.js[section.jsIndex]();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function showPassage(passageName: string) {
|
|
297
|
+
let passage = currentSection.passages && currentSection.passages[passageName];
|
|
298
|
+
const masterSection = story.sections[''];
|
|
299
|
+
if (!passage && masterSection && masterSection.passages) passage = masterSection.passages[passageName];
|
|
300
|
+
if (!passage) return;
|
|
301
|
+
setSeen(passageName);
|
|
302
|
+
if (masterSection && masterSection.passages) {
|
|
303
|
+
const masterPassage = masterSection.passages[''];
|
|
304
|
+
if (masterPassage) {
|
|
305
|
+
run(masterPassage);
|
|
306
|
+
ui.write(masterPassage.text || '');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const master = currentSection.passages && currentSection.passages[''];
|
|
310
|
+
if (master) {
|
|
311
|
+
run(master);
|
|
312
|
+
ui.write(master.text || '');
|
|
313
|
+
}
|
|
314
|
+
run(passage);
|
|
315
|
+
ui.write(passage.text || '');
|
|
316
|
+
save();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function processAttributes(attributes: string[]) {
|
|
320
|
+
attributes.forEach(function (attribute) {
|
|
321
|
+
if (startsWith(attribute, '@replace ')) {
|
|
322
|
+
replaceLabel(attribute.substring(9));
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
setAttribute(attribute);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function restart() {
|
|
331
|
+
if (settings.persist && window.localStorage && story.id) {
|
|
332
|
+
const keys = Object.keys(localStorage);
|
|
333
|
+
for (const key of keys) {
|
|
334
|
+
if (startsWith(key, story.id)) {
|
|
335
|
+
localStorage.removeItem(key);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
storageFallback = {};
|
|
341
|
+
}
|
|
342
|
+
if (settings.scroll === 'element') {
|
|
343
|
+
outputElement.innerHTML = '';
|
|
344
|
+
begin();
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
location.reload();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function save() {
|
|
352
|
+
set('_output', outputElement.innerHTML);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function load() {
|
|
356
|
+
const output = get('_output');
|
|
357
|
+
if (!output) return false;
|
|
358
|
+
outputElement.innerHTML = output;
|
|
359
|
+
const element = document.getElementById(get('_output-section'));
|
|
360
|
+
if (!element) return false;
|
|
361
|
+
currentSectionElement = element;
|
|
362
|
+
currentSection = story.sections[get('_section')];
|
|
363
|
+
const transition = get('_transition');
|
|
364
|
+
if (transition) {
|
|
365
|
+
eval('(' + transition + ')()');
|
|
366
|
+
}
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function setSeen(sectionName: string) {
|
|
371
|
+
let seenSections = get('_seen_sections');
|
|
372
|
+
if (!seenSections) seenSections = [];
|
|
373
|
+
if (seenSections.indexOf(sectionName) == -1) {
|
|
374
|
+
seenSections.push(sectionName);
|
|
375
|
+
set('_seen_sections', seenSections);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function seen(sectionName: string) {
|
|
380
|
+
const seenSections = get('_seen_sections');
|
|
381
|
+
if (!seenSections) return false;
|
|
382
|
+
return (seenSections.indexOf(sectionName) > -1);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function newSection() {
|
|
386
|
+
if (currentSectionElement) {
|
|
387
|
+
disableLinks(currentSectionElement.querySelectorAll('.squiffy-link'));
|
|
388
|
+
currentSectionElement.querySelectorAll('input').forEach(el => {
|
|
389
|
+
const attribute = el.getAttribute('data-attribute') || el.id;
|
|
390
|
+
if (attribute) set(attribute, el.value);
|
|
391
|
+
el.disabled = true;
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
currentSectionElement.querySelectorAll("[contenteditable]").forEach(el => {
|
|
395
|
+
const attribute = el.getAttribute('data-attribute') || el.id;
|
|
396
|
+
if (attribute) set(attribute, el.innerHTML);
|
|
397
|
+
(el as HTMLElement).contentEditable = 'false';
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
currentSectionElement.querySelectorAll('textarea').forEach(el => {
|
|
401
|
+
const attribute = el.getAttribute('data-attribute') || el.id;
|
|
402
|
+
if (attribute) set(attribute, el.value);
|
|
403
|
+
el.disabled = true;
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const sectionCount = get('_section-count') + 1;
|
|
408
|
+
set('_section-count', sectionCount);
|
|
409
|
+
const id = 'squiffy-section-' + sectionCount;
|
|
410
|
+
|
|
411
|
+
currentSectionElement = document.createElement('div');
|
|
412
|
+
currentSectionElement.id = id;
|
|
413
|
+
outputElement.appendChild(currentSectionElement);
|
|
414
|
+
|
|
415
|
+
set('_output-section', id);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const ui = {
|
|
419
|
+
write: (text: string) => {
|
|
420
|
+
if (!currentSectionElement) return;
|
|
421
|
+
scrollPosition = outputElement.scrollHeight;
|
|
422
|
+
|
|
423
|
+
const div = document.createElement('div');
|
|
424
|
+
currentSectionElement.appendChild(div);
|
|
425
|
+
div.innerHTML = ui.processText(text);
|
|
426
|
+
|
|
427
|
+
ui.scrollToEnd();
|
|
428
|
+
},
|
|
429
|
+
clearScreen: () => {
|
|
430
|
+
outputElement.innerHTML = '';
|
|
431
|
+
newSection();
|
|
432
|
+
},
|
|
433
|
+
scrollToEnd: () => {
|
|
434
|
+
if (settings.scroll === 'element') {
|
|
435
|
+
const scrollTo = outputElement.scrollHeight - outputElement.clientHeight;
|
|
436
|
+
const currentScrollTop = outputElement.scrollTop;
|
|
437
|
+
if (scrollTo > (currentScrollTop || 0)) {
|
|
438
|
+
outputElement.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
let scrollTo = scrollPosition;
|
|
443
|
+
const currentScrollTop = Math.max(document.body.scrollTop, document.documentElement.scrollTop);
|
|
444
|
+
if (scrollTo > currentScrollTop) {
|
|
445
|
+
const maxScrollTop = document.documentElement.scrollHeight - window.innerHeight;
|
|
446
|
+
if (scrollTo > maxScrollTop) scrollTo = maxScrollTop;
|
|
447
|
+
window.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
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
|
+
const data = {
|
|
619
|
+
fulltext: text
|
|
620
|
+
};
|
|
621
|
+
return process(text, data);
|
|
622
|
+
},
|
|
623
|
+
transition: function (f: any) {
|
|
624
|
+
set('_transition', f.toString());
|
|
625
|
+
f();
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
function startsWith(string: string, prefix: string) {
|
|
630
|
+
return string.substring(0, prefix.length) === prefix;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function rotate(options: string, current: string | null) {
|
|
634
|
+
const colon = options.indexOf(':');
|
|
635
|
+
if (colon == -1) {
|
|
636
|
+
return [options, current];
|
|
637
|
+
}
|
|
638
|
+
const next = options.substring(0, colon);
|
|
639
|
+
let remaining = options.substring(colon + 1);
|
|
640
|
+
if (current) remaining += ':' + current;
|
|
641
|
+
return [next, remaining];
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
outputElement = options.element;
|
|
645
|
+
story = options.story;
|
|
646
|
+
|
|
647
|
+
settings = {
|
|
648
|
+
scroll: options.scroll || 'body',
|
|
649
|
+
persist: (options.persist === undefined) ? true : options.persist,
|
|
650
|
+
onSet: options.onSet || (() => {})
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
if (options.persist === true && !story.id) {
|
|
654
|
+
console.warn("Persist is set to true in Squiffy runtime options, but no story id has been set. Persist will be disabled.");
|
|
655
|
+
settings.persist = false;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (settings.scroll === 'element') {
|
|
659
|
+
outputElement.style.overflowY = 'auto';
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
initLinkHandler();
|
|
663
|
+
begin();
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
restart: restart,
|
|
667
|
+
get: get,
|
|
668
|
+
set: set,
|
|
669
|
+
};
|
|
670
|
+
};
|
package/src/squiffy.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import yargs from 'yargs';
|
|
2
|
+
import { hideBin } from 'yargs/helpers';
|
|
3
|
+
import { SQUIFFY_VERSION } from './version.js';
|
|
4
|
+
import { createPackage } from './packager.js';
|
|
5
|
+
import { serve } from './server.js';
|
|
6
|
+
|
|
7
|
+
const argv = yargs(hideBin(process.argv))
|
|
8
|
+
.usage(
|
|
9
|
+
`Usage: $0 filename.squiffy [options]`)
|
|
10
|
+
.demand(1)
|
|
11
|
+
.alias('s', 'serve')
|
|
12
|
+
.alias('p', 'port')
|
|
13
|
+
.describe('s', 'Start HTTP server after compiling')
|
|
14
|
+
.describe('p', 'Port for HTTP server (only with --serve)')
|
|
15
|
+
.describe('scriptonly', 'Only generate JavaScript file (and optionally specify a name)')
|
|
16
|
+
.describe('zip', 'Create zip file')
|
|
17
|
+
.parseSync();
|
|
18
|
+
|
|
19
|
+
console.log('Squiffy ' + SQUIFFY_VERSION);
|
|
20
|
+
|
|
21
|
+
var options = {
|
|
22
|
+
serve: argv.s,
|
|
23
|
+
scriptOnly: argv.scriptonly,
|
|
24
|
+
pluginName: argv.pluginname,
|
|
25
|
+
zip: argv.zip,
|
|
26
|
+
write: true,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const inputFilename = argv._[0] as string;
|
|
30
|
+
|
|
31
|
+
var result = await createPackage(inputFilename);
|
|
32
|
+
|
|
33
|
+
if (result && options.serve) {
|
|
34
|
+
var port = (argv.p as number) || 8282;
|
|
35
|
+
serve(result, port);
|
|
36
|
+
}
|