squiffy-runtime 6.0.0-alpha.10 → 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 +1 -50
- package/dist/squiffy.runtime.js +13 -169
- package/dist/squiffy.runtime.test.js +16 -4
- 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 +2 -2
- package/src/__snapshots__/squiffy.runtime.test.ts.snap +0 -10
- package/src/events.ts +42 -0
- package/src/squiffy.runtime.test.ts +26 -4
- package/src/squiffy.runtime.ts +16 -244
- package/src/textProcessor.ts +177 -0
- package/src/types.ts +78 -0
- package/src/utils.ts +14 -0
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
|
+
}
|
|
@@ -1,51 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
element: HTMLElement;
|
|
3
|
-
story: Story;
|
|
4
|
-
scroll?: string;
|
|
5
|
-
persist?: boolean;
|
|
6
|
-
onSet?: (attribute: string, value: any) => void;
|
|
7
|
-
}
|
|
8
|
-
export interface SquiffySettings {
|
|
9
|
-
scroll: string;
|
|
10
|
-
persist: boolean;
|
|
11
|
-
onSet: (attribute: string, value: any) => void;
|
|
12
|
-
}
|
|
13
|
-
export interface SquiffyApi {
|
|
14
|
-
restart: () => void;
|
|
15
|
-
get: (attribute: string) => any;
|
|
16
|
-
set: (attribute: string, value: any) => void;
|
|
17
|
-
clickLink: (link: HTMLElement) => boolean;
|
|
18
|
-
update: (story: Story) => void;
|
|
19
|
-
}
|
|
20
|
-
interface SquiffyJsFunctionApi {
|
|
21
|
-
get: (attribute: string) => any;
|
|
22
|
-
set: (attribute: string, value: any) => void;
|
|
23
|
-
ui: {
|
|
24
|
-
transition: (f: any) => void;
|
|
25
|
-
};
|
|
26
|
-
story: {
|
|
27
|
-
go: (section: string) => void;
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
export interface Story {
|
|
31
|
-
js: ((squiffy: SquiffyJsFunctionApi, get: (attribute: string) => any, set: (attribute: string, value: any) => void) => void)[];
|
|
32
|
-
start: string;
|
|
33
|
-
id?: string | null;
|
|
34
|
-
sections: Record<string, Section>;
|
|
35
|
-
}
|
|
36
|
-
export interface Section {
|
|
37
|
-
text?: string;
|
|
38
|
-
clear?: boolean;
|
|
39
|
-
attributes?: string[];
|
|
40
|
-
jsIndex?: number;
|
|
41
|
-
passages?: Record<string, Passage>;
|
|
42
|
-
passageCount?: number;
|
|
43
|
-
}
|
|
44
|
-
export interface Passage {
|
|
45
|
-
text?: string;
|
|
46
|
-
clear?: boolean;
|
|
47
|
-
attributes?: string[];
|
|
48
|
-
jsIndex?: number;
|
|
49
|
-
}
|
|
1
|
+
import { SquiffyApi, SquiffyInitOptions } from './types.js';
|
|
50
2
|
export declare const init: (options: SquiffyInitOptions) => SquiffyApi;
|
|
51
|
-
export {};
|
package/dist/squiffy.runtime.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { startsWith, rotate } from "./utils.js";
|
|
2
|
+
import { TextProcessor } from './textProcessor.js';
|
|
3
|
+
import { Emitter } from './events.js';
|
|
1
4
|
export const init = (options) => {
|
|
2
5
|
let story;
|
|
3
6
|
let currentSection;
|
|
@@ -7,6 +10,8 @@ export const init = (options) => {
|
|
|
7
10
|
let outputElement;
|
|
8
11
|
let settings;
|
|
9
12
|
let storageFallback = {};
|
|
13
|
+
let textProcessor;
|
|
14
|
+
const emitter = new Emitter();
|
|
10
15
|
function set(attribute, value) {
|
|
11
16
|
if (typeof value === 'undefined')
|
|
12
17
|
value = true;
|
|
@@ -58,6 +63,7 @@ export const init = (options) => {
|
|
|
58
63
|
showPassage('@last');
|
|
59
64
|
}
|
|
60
65
|
}
|
|
66
|
+
emitter.emit('linkClick', { linkType: 'passage' });
|
|
61
67
|
return true;
|
|
62
68
|
}
|
|
63
69
|
if (section) {
|
|
@@ -65,6 +71,7 @@ export const init = (options) => {
|
|
|
65
71
|
if (section) {
|
|
66
72
|
go(section);
|
|
67
73
|
}
|
|
74
|
+
emitter.emit('linkClick', { linkType: 'section' });
|
|
68
75
|
return true;
|
|
69
76
|
}
|
|
70
77
|
if (rotateOrSequenceAttr) {
|
|
@@ -80,6 +87,7 @@ export const init = (options) => {
|
|
|
80
87
|
set(attribute, result[0]);
|
|
81
88
|
}
|
|
82
89
|
save();
|
|
90
|
+
emitter.emit('linkClick', { linkType: rotateAttr ? 'rotate' : 'sequence' });
|
|
83
91
|
return true;
|
|
84
92
|
}
|
|
85
93
|
return false;
|
|
@@ -408,184 +416,16 @@ export const init = (options) => {
|
|
|
408
416
|
}
|
|
409
417
|
},
|
|
410
418
|
processText: (text) => {
|
|
411
|
-
function process(text, data) {
|
|
412
|
-
let containsUnprocessedSection = false;
|
|
413
|
-
const open = text.indexOf('{');
|
|
414
|
-
let close;
|
|
415
|
-
if (open > -1) {
|
|
416
|
-
let nestCount = 1;
|
|
417
|
-
let searchStart = open + 1;
|
|
418
|
-
let finished = false;
|
|
419
|
-
while (!finished) {
|
|
420
|
-
const nextOpen = text.indexOf('{', searchStart);
|
|
421
|
-
const nextClose = text.indexOf('}', searchStart);
|
|
422
|
-
if (nextClose > -1) {
|
|
423
|
-
if (nextOpen > -1 && nextOpen < nextClose) {
|
|
424
|
-
nestCount++;
|
|
425
|
-
searchStart = nextOpen + 1;
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
nestCount--;
|
|
429
|
-
searchStart = nextClose + 1;
|
|
430
|
-
if (nestCount === 0) {
|
|
431
|
-
close = nextClose;
|
|
432
|
-
containsUnprocessedSection = true;
|
|
433
|
-
finished = true;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
finished = true;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
if (containsUnprocessedSection) {
|
|
443
|
-
const section = text.substring(open + 1, close);
|
|
444
|
-
const value = processTextCommand(section, data);
|
|
445
|
-
text = text.substring(0, open) + value + process(text.substring(close + 1), data);
|
|
446
|
-
}
|
|
447
|
-
return (text);
|
|
448
|
-
}
|
|
449
|
-
function processTextCommand(text, data) {
|
|
450
|
-
if (startsWith(text, 'if ')) {
|
|
451
|
-
return processTextCommand_If(text, data);
|
|
452
|
-
}
|
|
453
|
-
else if (startsWith(text, 'else:')) {
|
|
454
|
-
return processTextCommand_Else(text, data);
|
|
455
|
-
}
|
|
456
|
-
else if (startsWith(text, 'label:')) {
|
|
457
|
-
return processTextCommand_Label(text, data);
|
|
458
|
-
}
|
|
459
|
-
else if (/^rotate[: ]/.test(text)) {
|
|
460
|
-
return processTextCommand_Rotate('rotate', text);
|
|
461
|
-
}
|
|
462
|
-
else if (/^sequence[: ]/.test(text)) {
|
|
463
|
-
return processTextCommand_Rotate('sequence', text);
|
|
464
|
-
}
|
|
465
|
-
else if (currentSection.passages && text in currentSection.passages) {
|
|
466
|
-
return process(currentSection.passages[text].text || '', data);
|
|
467
|
-
}
|
|
468
|
-
else if (text in story.sections) {
|
|
469
|
-
return process(story.sections[text].text || '', data);
|
|
470
|
-
}
|
|
471
|
-
else if (startsWith(text, '@') && !startsWith(text, '@replace')) {
|
|
472
|
-
processAttributes(text.substring(1).split(","));
|
|
473
|
-
return "";
|
|
474
|
-
}
|
|
475
|
-
return get(text);
|
|
476
|
-
}
|
|
477
|
-
function processTextCommand_If(section, data) {
|
|
478
|
-
const command = section.substring(3);
|
|
479
|
-
const colon = command.indexOf(':');
|
|
480
|
-
if (colon == -1) {
|
|
481
|
-
return ('{if ' + command + '}');
|
|
482
|
-
}
|
|
483
|
-
const text = command.substring(colon + 1);
|
|
484
|
-
let condition = command.substring(0, colon);
|
|
485
|
-
condition = condition.replace("<", "<");
|
|
486
|
-
const operatorRegex = /([\w ]*)(=|<=|>=|<>|<|>)(.*)/;
|
|
487
|
-
const match = operatorRegex.exec(condition);
|
|
488
|
-
let result = false;
|
|
489
|
-
if (match) {
|
|
490
|
-
const lhs = get(match[1]);
|
|
491
|
-
const op = match[2];
|
|
492
|
-
let rhs = match[3];
|
|
493
|
-
if (startsWith(rhs, '@'))
|
|
494
|
-
rhs = get(rhs.substring(1));
|
|
495
|
-
if (op == '=' && lhs == rhs)
|
|
496
|
-
result = true;
|
|
497
|
-
if (op == '<>' && lhs != rhs)
|
|
498
|
-
result = true;
|
|
499
|
-
if (op == '>' && lhs > rhs)
|
|
500
|
-
result = true;
|
|
501
|
-
if (op == '<' && lhs < rhs)
|
|
502
|
-
result = true;
|
|
503
|
-
if (op == '>=' && lhs >= rhs)
|
|
504
|
-
result = true;
|
|
505
|
-
if (op == '<=' && lhs <= rhs)
|
|
506
|
-
result = true;
|
|
507
|
-
}
|
|
508
|
-
else {
|
|
509
|
-
let checkValue = true;
|
|
510
|
-
if (startsWith(condition, 'not ')) {
|
|
511
|
-
condition = condition.substring(4);
|
|
512
|
-
checkValue = false;
|
|
513
|
-
}
|
|
514
|
-
if (startsWith(condition, 'seen ')) {
|
|
515
|
-
result = (seen(condition.substring(5)) == checkValue);
|
|
516
|
-
}
|
|
517
|
-
else {
|
|
518
|
-
let value = get(condition);
|
|
519
|
-
if (value === null)
|
|
520
|
-
value = false;
|
|
521
|
-
result = (value == checkValue);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
const textResult = result ? process(text, data) : '';
|
|
525
|
-
data.lastIf = result;
|
|
526
|
-
return textResult;
|
|
527
|
-
}
|
|
528
|
-
function processTextCommand_Else(section, data) {
|
|
529
|
-
if (!('lastIf' in data) || data.lastIf)
|
|
530
|
-
return '';
|
|
531
|
-
const text = section.substring(5);
|
|
532
|
-
return process(text, data);
|
|
533
|
-
}
|
|
534
|
-
function processTextCommand_Label(section, data) {
|
|
535
|
-
const command = section.substring(6);
|
|
536
|
-
const eq = command.indexOf('=');
|
|
537
|
-
if (eq == -1) {
|
|
538
|
-
return ('{label:' + command + '}');
|
|
539
|
-
}
|
|
540
|
-
const text = command.substring(eq + 1);
|
|
541
|
-
const label = command.substring(0, eq);
|
|
542
|
-
return '<span class="squiffy-label-' + label + '">' + process(text, data) + '</span>';
|
|
543
|
-
}
|
|
544
|
-
function processTextCommand_Rotate(type, section) {
|
|
545
|
-
let options;
|
|
546
|
-
let attribute = '';
|
|
547
|
-
if (section.substring(type.length, type.length + 1) == ' ') {
|
|
548
|
-
const colon = section.indexOf(':');
|
|
549
|
-
if (colon == -1) {
|
|
550
|
-
return '{' + section + '}';
|
|
551
|
-
}
|
|
552
|
-
options = section.substring(colon + 1);
|
|
553
|
-
attribute = section.substring(type.length + 1, colon);
|
|
554
|
-
}
|
|
555
|
-
else {
|
|
556
|
-
options = section.substring(type.length + 1);
|
|
557
|
-
}
|
|
558
|
-
// TODO: Check - previously there was no second parameter here
|
|
559
|
-
const rotation = rotate(options.replace(/"/g, '"').replace(/'/g, '''), null);
|
|
560
|
-
if (attribute) {
|
|
561
|
-
set(attribute, rotation[0]);
|
|
562
|
-
}
|
|
563
|
-
return '<a class="squiffy-link" data-' + type + '="' + rotation[1] + '" data-attribute="' + attribute + '" role="link">' + rotation[0] + '</a>';
|
|
564
|
-
}
|
|
565
419
|
const data = {
|
|
566
420
|
fulltext: text
|
|
567
421
|
};
|
|
568
|
-
return process(text, data);
|
|
422
|
+
return textProcessor.process(text, data);
|
|
569
423
|
},
|
|
570
424
|
transition: function (f) {
|
|
571
425
|
set('_transition', f.toString());
|
|
572
426
|
f();
|
|
573
427
|
},
|
|
574
428
|
};
|
|
575
|
-
function startsWith(string, prefix) {
|
|
576
|
-
return string.substring(0, prefix.length) === prefix;
|
|
577
|
-
}
|
|
578
|
-
function rotate(options, current) {
|
|
579
|
-
const colon = options.indexOf(':');
|
|
580
|
-
if (colon == -1) {
|
|
581
|
-
return [options, current];
|
|
582
|
-
}
|
|
583
|
-
const next = options.substring(0, colon);
|
|
584
|
-
let remaining = options.substring(colon + 1);
|
|
585
|
-
if (current)
|
|
586
|
-
remaining += ':' + current;
|
|
587
|
-
return [next, remaining];
|
|
588
|
-
}
|
|
589
429
|
function safeQuerySelector(name) {
|
|
590
430
|
return name.replace(/'/g, "\\'");
|
|
591
431
|
}
|
|
@@ -674,6 +514,7 @@ export const init = (options) => {
|
|
|
674
514
|
return;
|
|
675
515
|
handleClick(event);
|
|
676
516
|
});
|
|
517
|
+
textProcessor = new TextProcessor(get, set, story, ui, seen, processAttributes);
|
|
677
518
|
begin();
|
|
678
519
|
return {
|
|
679
520
|
restart: restart,
|
|
@@ -681,5 +522,8 @@ export const init = (options) => {
|
|
|
681
522
|
set: set,
|
|
682
523
|
clickLink: handleLink,
|
|
683
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),
|
|
684
528
|
};
|
|
685
529
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { expect, test, beforeEach, afterEach } from 'vitest';
|
|
1
|
+
import { expect, test, beforeEach, afterEach, vi } from 'vitest';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import globalJsdom from 'global-jsdom';
|
|
4
4
|
import { init } from './squiffy.runtime.js';
|
|
@@ -18,7 +18,6 @@ const html = `
|
|
|
18
18
|
`;
|
|
19
19
|
const compile = async (script) => {
|
|
20
20
|
const compileResult = await squiffyCompile({
|
|
21
|
-
scriptBaseFilename: "filename.squiffy", // TODO: This shouldn't be required
|
|
22
21
|
script: script,
|
|
23
22
|
});
|
|
24
23
|
if (!compileResult.success) {
|
|
@@ -34,7 +33,6 @@ const compile = async (script) => {
|
|
|
34
33
|
};
|
|
35
34
|
};
|
|
36
35
|
const initScript = async (script) => {
|
|
37
|
-
globalJsdom(html);
|
|
38
36
|
const element = document.getElementById('squiffy');
|
|
39
37
|
if (!element) {
|
|
40
38
|
throw new Error('Element not found');
|
|
@@ -64,7 +62,7 @@ const getTestOutput = () => {
|
|
|
64
62
|
};
|
|
65
63
|
let cleanup;
|
|
66
64
|
beforeEach(() => {
|
|
67
|
-
cleanup = globalJsdom();
|
|
65
|
+
cleanup = globalJsdom(html);
|
|
68
66
|
});
|
|
69
67
|
afterEach(() => {
|
|
70
68
|
cleanup();
|
|
@@ -83,11 +81,18 @@ test('Click a section link', async () => {
|
|
|
83
81
|
expect(linkToPassage).not.toBeNull();
|
|
84
82
|
expect(section3Link).not.toBeNull();
|
|
85
83
|
expect(linkToPassage.classList).not.toContain('disabled');
|
|
84
|
+
const handler = vi.fn();
|
|
85
|
+
const off = squiffyApi.on('linkClick', handler);
|
|
86
86
|
const handled = squiffyApi.clickLink(section3Link);
|
|
87
87
|
expect(handled).toBe(true);
|
|
88
88
|
expect(element.innerHTML).toMatchSnapshot();
|
|
89
89
|
// passage link is from the previous section, so should be unclickable
|
|
90
90
|
expect(squiffyApi.clickLink(linkToPassage)).toBe(false);
|
|
91
|
+
// await for the event to be processed
|
|
92
|
+
await Promise.resolve();
|
|
93
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
94
|
+
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ linkType: 'section' }));
|
|
95
|
+
off();
|
|
91
96
|
});
|
|
92
97
|
test('Click a passage link', async () => {
|
|
93
98
|
const script = await fs.readFile('../examples/test/example.squiffy', 'utf-8');
|
|
@@ -99,12 +104,19 @@ test('Click a passage link', async () => {
|
|
|
99
104
|
expect(linkToPassage).not.toBeNull();
|
|
100
105
|
expect(section3Link).not.toBeNull();
|
|
101
106
|
expect(linkToPassage.classList).not.toContain('disabled');
|
|
107
|
+
const handler = vi.fn();
|
|
108
|
+
const off = squiffyApi.on('linkClick', handler);
|
|
102
109
|
const handled = squiffyApi.clickLink(linkToPassage);
|
|
103
110
|
expect(handled).toBe(true);
|
|
104
111
|
expect(linkToPassage.classList).toContain('disabled');
|
|
105
112
|
expect(element.innerHTML).toMatchSnapshot();
|
|
106
113
|
// shouldn't be able to click it again
|
|
107
114
|
expect(squiffyApi.clickLink(linkToPassage)).toBe(false);
|
|
115
|
+
// await for the event to be processed
|
|
116
|
+
await Promise.resolve();
|
|
117
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
118
|
+
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ linkType: 'passage' }));
|
|
119
|
+
off();
|
|
108
120
|
});
|
|
109
121
|
test('Run JavaScript functions', async () => {
|
|
110
122
|
const script = `
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class TextProcessor {
|
|
2
|
+
get: (attribute: string) => any;
|
|
3
|
+
set: (attribute: string, value: any) => void;
|
|
4
|
+
story: any;
|
|
5
|
+
currentSection: any;
|
|
6
|
+
seen: (section: string) => boolean;
|
|
7
|
+
processAttributes: (attributes: string[]) => void;
|
|
8
|
+
constructor(get: (attribute: string) => any, set: (attribute: string, value: any) => void, story: any, currentSection: any, seen: (section: string) => boolean, processAttributes: (attributes: string[]) => void);
|
|
9
|
+
process(text: string, data: any): string;
|
|
10
|
+
processTextCommand(text: string, data: any): any;
|
|
11
|
+
processTextCommand_If(section: string, data: any): string;
|
|
12
|
+
processTextCommand_Else(section: string, data: any): string;
|
|
13
|
+
processTextCommand_Label(section: string, data: any): string;
|
|
14
|
+
processTextCommand_Rotate(type: string, section: string): string;
|
|
15
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { startsWith, rotate } from "./utils.js";
|
|
2
|
+
export class TextProcessor {
|
|
3
|
+
constructor(get, set, story, currentSection, seen, processAttributes) {
|
|
4
|
+
this.get = get;
|
|
5
|
+
this.set = set;
|
|
6
|
+
this.story = story;
|
|
7
|
+
this.currentSection = currentSection;
|
|
8
|
+
this.seen = seen;
|
|
9
|
+
this.processAttributes = processAttributes;
|
|
10
|
+
}
|
|
11
|
+
process(text, data) {
|
|
12
|
+
let containsUnprocessedSection = false;
|
|
13
|
+
const open = text.indexOf('{');
|
|
14
|
+
let close;
|
|
15
|
+
if (open > -1) {
|
|
16
|
+
let nestCount = 1;
|
|
17
|
+
let searchStart = open + 1;
|
|
18
|
+
let finished = false;
|
|
19
|
+
while (!finished) {
|
|
20
|
+
const nextOpen = text.indexOf('{', searchStart);
|
|
21
|
+
const nextClose = text.indexOf('}', searchStart);
|
|
22
|
+
if (nextClose > -1) {
|
|
23
|
+
if (nextOpen > -1 && nextOpen < nextClose) {
|
|
24
|
+
nestCount++;
|
|
25
|
+
searchStart = nextOpen + 1;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
nestCount--;
|
|
29
|
+
searchStart = nextClose + 1;
|
|
30
|
+
if (nestCount === 0) {
|
|
31
|
+
close = nextClose;
|
|
32
|
+
containsUnprocessedSection = true;
|
|
33
|
+
finished = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
finished = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (containsUnprocessedSection) {
|
|
43
|
+
const section = text.substring(open + 1, close);
|
|
44
|
+
const value = this.processTextCommand(section, data);
|
|
45
|
+
text = text.substring(0, open) + value + this.process(text.substring(close + 1), data);
|
|
46
|
+
}
|
|
47
|
+
return (text);
|
|
48
|
+
}
|
|
49
|
+
processTextCommand(text, data) {
|
|
50
|
+
if (startsWith(text, 'if ')) {
|
|
51
|
+
return this.processTextCommand_If(text, data);
|
|
52
|
+
}
|
|
53
|
+
else if (startsWith(text, 'else:')) {
|
|
54
|
+
return this.processTextCommand_Else(text, data);
|
|
55
|
+
}
|
|
56
|
+
else if (startsWith(text, 'label:')) {
|
|
57
|
+
return this.processTextCommand_Label(text, data);
|
|
58
|
+
}
|
|
59
|
+
else if (/^rotate[: ]/.test(text)) {
|
|
60
|
+
return this.processTextCommand_Rotate('rotate', text);
|
|
61
|
+
}
|
|
62
|
+
else if (/^sequence[: ]/.test(text)) {
|
|
63
|
+
return this.processTextCommand_Rotate('sequence', text);
|
|
64
|
+
}
|
|
65
|
+
else if (this.currentSection.passages && text in this.currentSection.passages) {
|
|
66
|
+
return this.process(this.currentSection.passages[text].text || '', data);
|
|
67
|
+
}
|
|
68
|
+
else if (text in this.story.sections) {
|
|
69
|
+
return this.process(this.story.sections[text].text || '', data);
|
|
70
|
+
}
|
|
71
|
+
else if (startsWith(text, '@') && !startsWith(text, '@replace')) {
|
|
72
|
+
this.processAttributes(text.substring(1).split(","));
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
return this.get(text);
|
|
76
|
+
}
|
|
77
|
+
processTextCommand_If(section, data) {
|
|
78
|
+
const command = section.substring(3);
|
|
79
|
+
const colon = command.indexOf(':');
|
|
80
|
+
if (colon == -1) {
|
|
81
|
+
return ('{if ' + command + '}');
|
|
82
|
+
}
|
|
83
|
+
const text = command.substring(colon + 1);
|
|
84
|
+
let condition = command.substring(0, colon);
|
|
85
|
+
condition = condition.replace("<", "<");
|
|
86
|
+
const operatorRegex = /([\w ]*)(=|<=|>=|<>|<|>)(.*)/;
|
|
87
|
+
const match = operatorRegex.exec(condition);
|
|
88
|
+
let result = false;
|
|
89
|
+
if (match) {
|
|
90
|
+
const lhs = this.get(match[1]);
|
|
91
|
+
const op = match[2];
|
|
92
|
+
let rhs = match[3];
|
|
93
|
+
if (startsWith(rhs, '@'))
|
|
94
|
+
rhs = this.get(rhs.substring(1));
|
|
95
|
+
if (op == '=' && lhs == rhs)
|
|
96
|
+
result = true;
|
|
97
|
+
if (op == '<>' && lhs != rhs)
|
|
98
|
+
result = true;
|
|
99
|
+
if (op == '>' && lhs > rhs)
|
|
100
|
+
result = true;
|
|
101
|
+
if (op == '<' && lhs < rhs)
|
|
102
|
+
result = true;
|
|
103
|
+
if (op == '>=' && lhs >= rhs)
|
|
104
|
+
result = true;
|
|
105
|
+
if (op == '<=' && lhs <= rhs)
|
|
106
|
+
result = true;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
let checkValue = true;
|
|
110
|
+
if (startsWith(condition, 'not ')) {
|
|
111
|
+
condition = condition.substring(4);
|
|
112
|
+
checkValue = false;
|
|
113
|
+
}
|
|
114
|
+
if (startsWith(condition, 'seen ')) {
|
|
115
|
+
result = (this.seen(condition.substring(5)) == checkValue);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
let value = this.get(condition);
|
|
119
|
+
if (value === null)
|
|
120
|
+
value = false;
|
|
121
|
+
result = (value == checkValue);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const textResult = result ? this.process(text, data) : '';
|
|
125
|
+
data.lastIf = result;
|
|
126
|
+
return textResult;
|
|
127
|
+
}
|
|
128
|
+
processTextCommand_Else(section, data) {
|
|
129
|
+
if (!('lastIf' in data) || data.lastIf)
|
|
130
|
+
return '';
|
|
131
|
+
const text = section.substring(5);
|
|
132
|
+
return this.process(text, data);
|
|
133
|
+
}
|
|
134
|
+
processTextCommand_Label(section, data) {
|
|
135
|
+
const command = section.substring(6);
|
|
136
|
+
const eq = command.indexOf('=');
|
|
137
|
+
if (eq == -1) {
|
|
138
|
+
return ('{label:' + command + '}');
|
|
139
|
+
}
|
|
140
|
+
const text = command.substring(eq + 1);
|
|
141
|
+
const label = command.substring(0, eq);
|
|
142
|
+
return '<span class="squiffy-label-' + label + '">' + this.process(text, data) + '</span>';
|
|
143
|
+
}
|
|
144
|
+
processTextCommand_Rotate(type, section) {
|
|
145
|
+
let options;
|
|
146
|
+
let attribute = '';
|
|
147
|
+
if (section.substring(type.length, type.length + 1) == ' ') {
|
|
148
|
+
const colon = section.indexOf(':');
|
|
149
|
+
if (colon == -1) {
|
|
150
|
+
return '{' + section + '}';
|
|
151
|
+
}
|
|
152
|
+
options = section.substring(colon + 1);
|
|
153
|
+
attribute = section.substring(type.length + 1, colon);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
options = section.substring(type.length + 1);
|
|
157
|
+
}
|
|
158
|
+
// TODO: Check - previously there was no second parameter here
|
|
159
|
+
const rotation = rotate(options.replace(/"/g, '"').replace(/'/g, '''), null);
|
|
160
|
+
if (attribute) {
|
|
161
|
+
this.set(attribute, rotation[0]);
|
|
162
|
+
}
|
|
163
|
+
return '<a class="squiffy-link" data-' + type + '="' + rotation[1] + '" data-attribute="' + attribute + '" role="link">' + rotation[0] + '</a>';
|
|
164
|
+
}
|
|
165
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { SquiffyEventHandler, SquiffyEventMap } from "./events.js";
|
|
2
|
+
export interface SquiffyInitOptions {
|
|
3
|
+
element: HTMLElement;
|
|
4
|
+
story: Story;
|
|
5
|
+
scroll?: string;
|
|
6
|
+
persist?: boolean;
|
|
7
|
+
onSet?: (attribute: string, value: any) => void;
|
|
8
|
+
}
|
|
9
|
+
export interface SquiffySettings {
|
|
10
|
+
scroll: string;
|
|
11
|
+
persist: boolean;
|
|
12
|
+
onSet: (attribute: string, value: any) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface SquiffyApi {
|
|
15
|
+
restart: () => void;
|
|
16
|
+
get: (attribute: string) => any;
|
|
17
|
+
set: (attribute: string, value: any) => void;
|
|
18
|
+
clickLink: (link: HTMLElement) => boolean;
|
|
19
|
+
update: (story: Story) => void;
|
|
20
|
+
on<E extends keyof SquiffyEventMap>(event: E, handler: SquiffyEventHandler<E>): () => void;
|
|
21
|
+
off<E extends keyof SquiffyEventMap>(event: E, handler: SquiffyEventHandler<E>): void;
|
|
22
|
+
once<E extends keyof SquiffyEventMap>(event: E, handler: SquiffyEventHandler<E>): () => void;
|
|
23
|
+
}
|
|
24
|
+
interface SquiffyJsFunctionApi {
|
|
25
|
+
get: (attribute: string) => any;
|
|
26
|
+
set: (attribute: string, value: any) => void;
|
|
27
|
+
ui: {
|
|
28
|
+
transition: (f: any) => void;
|
|
29
|
+
};
|
|
30
|
+
story: {
|
|
31
|
+
go: (section: string) => void;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface Story {
|
|
35
|
+
js: ((squiffy: SquiffyJsFunctionApi, get: (attribute: string) => any, set: (attribute: string, value: any) => void) => void)[];
|
|
36
|
+
start: string;
|
|
37
|
+
id?: string | null;
|
|
38
|
+
sections: Record<string, Section>;
|
|
39
|
+
}
|
|
40
|
+
export interface Section {
|
|
41
|
+
text?: string;
|
|
42
|
+
clear?: boolean;
|
|
43
|
+
attributes?: string[];
|
|
44
|
+
jsIndex?: number;
|
|
45
|
+
passages?: Record<string, Passage>;
|
|
46
|
+
passageCount?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface Passage {
|
|
49
|
+
text?: string;
|
|
50
|
+
clear?: boolean;
|
|
51
|
+
attributes?: string[];
|
|
52
|
+
jsIndex?: number;
|
|
53
|
+
}
|
|
54
|
+
export {};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function startsWith(string, prefix) {
|
|
2
|
+
return string.substring(0, prefix.length) === prefix;
|
|
3
|
+
}
|
|
4
|
+
export function rotate(options, current) {
|
|
5
|
+
const colon = options.indexOf(':');
|
|
6
|
+
if (colon == -1) {
|
|
7
|
+
return [options, current];
|
|
8
|
+
}
|
|
9
|
+
const next = options.substring(0, colon);
|
|
10
|
+
let remaining = options.substring(colon + 1);
|
|
11
|
+
if (current)
|
|
12
|
+
remaining += ':' + current;
|
|
13
|
+
return [next, remaining];
|
|
14
|
+
}
|