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
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { startsWith, rotate } from "./utils.js";
|
|
2
|
+
|
|
3
|
+
export class TextProcessor {
|
|
4
|
+
get: (attribute: string) => any;
|
|
5
|
+
set: (attribute: string, value: any) => void;
|
|
6
|
+
story: any;
|
|
7
|
+
currentSection: any;
|
|
8
|
+
seen: (section: string) => boolean;
|
|
9
|
+
processAttributes: (attributes: string[]) => void;
|
|
10
|
+
|
|
11
|
+
constructor (get: (attribute: string) => any,
|
|
12
|
+
set: (attribute: string, value: any) => void,
|
|
13
|
+
story: any, currentSection: any,
|
|
14
|
+
seen: (section: string) => boolean,
|
|
15
|
+
processAttributes: (attributes: string[]) => void) {
|
|
16
|
+
this.get = get;
|
|
17
|
+
this.set = set;
|
|
18
|
+
this.story = story;
|
|
19
|
+
this.currentSection = currentSection;
|
|
20
|
+
this.seen = seen;
|
|
21
|
+
this.processAttributes = processAttributes;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
process(text: string, data: any) {
|
|
25
|
+
let containsUnprocessedSection = false;
|
|
26
|
+
const open = text.indexOf('{');
|
|
27
|
+
let close;
|
|
28
|
+
|
|
29
|
+
if (open > -1) {
|
|
30
|
+
let nestCount = 1;
|
|
31
|
+
let searchStart = open + 1;
|
|
32
|
+
let finished = false;
|
|
33
|
+
|
|
34
|
+
while (!finished) {
|
|
35
|
+
const nextOpen = text.indexOf('{', searchStart);
|
|
36
|
+
const nextClose = text.indexOf('}', searchStart);
|
|
37
|
+
|
|
38
|
+
if (nextClose > -1) {
|
|
39
|
+
if (nextOpen > -1 && nextOpen < nextClose) {
|
|
40
|
+
nestCount++;
|
|
41
|
+
searchStart = nextOpen + 1;
|
|
42
|
+
} else {
|
|
43
|
+
nestCount--;
|
|
44
|
+
searchStart = nextClose + 1;
|
|
45
|
+
if (nestCount === 0) {
|
|
46
|
+
close = nextClose;
|
|
47
|
+
containsUnprocessedSection = true;
|
|
48
|
+
finished = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
finished = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (containsUnprocessedSection) {
|
|
58
|
+
const section = text.substring(open + 1, close);
|
|
59
|
+
const value = this.processTextCommand(section, data);
|
|
60
|
+
text = text.substring(0, open) + value + this.process(text.substring(close! + 1), data);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (text);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
processTextCommand(text: string, data: any) {
|
|
67
|
+
if (startsWith(text, 'if ')) {
|
|
68
|
+
return this.processTextCommand_If(text, data);
|
|
69
|
+
} else if (startsWith(text, 'else:')) {
|
|
70
|
+
return this.processTextCommand_Else(text, data);
|
|
71
|
+
} else if (startsWith(text, 'label:')) {
|
|
72
|
+
return this.processTextCommand_Label(text, data);
|
|
73
|
+
} else if (/^rotate[: ]/.test(text)) {
|
|
74
|
+
return this.processTextCommand_Rotate('rotate', text);
|
|
75
|
+
} else if (/^sequence[: ]/.test(text)) {
|
|
76
|
+
return this.processTextCommand_Rotate('sequence', text);
|
|
77
|
+
} else if (this.currentSection.passages && text in this.currentSection.passages) {
|
|
78
|
+
return this.process(this.currentSection.passages[text].text || '', data);
|
|
79
|
+
} else if (text in this.story.sections) {
|
|
80
|
+
return this.process(this.story.sections[text].text || '', data);
|
|
81
|
+
} else if (startsWith(text, '@') && !startsWith(text, '@replace')) {
|
|
82
|
+
this.processAttributes(text.substring(1).split(","));
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
return this.get(text);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
processTextCommand_If(section: string, data: any) {
|
|
89
|
+
const command = section.substring(3);
|
|
90
|
+
const colon = command.indexOf(':');
|
|
91
|
+
if (colon == -1) {
|
|
92
|
+
return ('{if ' + command + '}');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const text = command.substring(colon + 1);
|
|
96
|
+
let condition = command.substring(0, colon);
|
|
97
|
+
condition = condition.replace("<", "<");
|
|
98
|
+
const operatorRegex = /([\w ]*)(=|<=|>=|<>|<|>)(.*)/;
|
|
99
|
+
const match = operatorRegex.exec(condition);
|
|
100
|
+
|
|
101
|
+
let result = false;
|
|
102
|
+
|
|
103
|
+
if (match) {
|
|
104
|
+
const lhs = this.get(match[1]);
|
|
105
|
+
const op = match[2];
|
|
106
|
+
let rhs = match[3];
|
|
107
|
+
|
|
108
|
+
if (startsWith(rhs, '@')) rhs = this.get(rhs.substring(1));
|
|
109
|
+
|
|
110
|
+
if (op == '=' && lhs == rhs) result = true;
|
|
111
|
+
if (op == '<>' && lhs != rhs) result = true;
|
|
112
|
+
if (op == '>' && lhs > rhs) result = true;
|
|
113
|
+
if (op == '<' && lhs < rhs) result = true;
|
|
114
|
+
if (op == '>=' && lhs >= rhs) result = true;
|
|
115
|
+
if (op == '<=' && lhs <= rhs) result = true;
|
|
116
|
+
} else {
|
|
117
|
+
let checkValue = true;
|
|
118
|
+
if (startsWith(condition, 'not ')) {
|
|
119
|
+
condition = condition.substring(4);
|
|
120
|
+
checkValue = false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (startsWith(condition, 'seen ')) {
|
|
124
|
+
result = (this.seen(condition.substring(5)) == checkValue);
|
|
125
|
+
} else {
|
|
126
|
+
let value = this.get(condition);
|
|
127
|
+
if (value === null) value = false;
|
|
128
|
+
result = (value == checkValue);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const textResult = result ? this.process(text, data) : '';
|
|
133
|
+
|
|
134
|
+
data.lastIf = result;
|
|
135
|
+
return textResult;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
processTextCommand_Else(section: string, data: any) {
|
|
139
|
+
if (!('lastIf' in data) || data.lastIf) return '';
|
|
140
|
+
const text = section.substring(5);
|
|
141
|
+
return this.process(text, data);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
processTextCommand_Label(section: string, data: any) {
|
|
145
|
+
const command = section.substring(6);
|
|
146
|
+
const eq = command.indexOf('=');
|
|
147
|
+
if (eq == -1) {
|
|
148
|
+
return ('{label:' + command + '}');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const text = command.substring(eq + 1);
|
|
152
|
+
const label = command.substring(0, eq);
|
|
153
|
+
|
|
154
|
+
return '<span class="squiffy-label-' + label + '">' + this.process(text, data) + '</span>';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
processTextCommand_Rotate(type: string, section: string) {
|
|
158
|
+
let options;
|
|
159
|
+
let attribute = '';
|
|
160
|
+
if (section.substring(type.length, type.length + 1) == ' ') {
|
|
161
|
+
const colon = section.indexOf(':');
|
|
162
|
+
if (colon == -1) {
|
|
163
|
+
return '{' + section + '}';
|
|
164
|
+
}
|
|
165
|
+
options = section.substring(colon + 1);
|
|
166
|
+
attribute = section.substring(type.length + 1, colon);
|
|
167
|
+
} else {
|
|
168
|
+
options = section.substring(type.length + 1);
|
|
169
|
+
}
|
|
170
|
+
// TODO: Check - previously there was no second parameter here
|
|
171
|
+
const rotation = rotate(options.replace(/"/g, '"').replace(/'/g, '''), null);
|
|
172
|
+
if (attribute) {
|
|
173
|
+
this.set(attribute, rotation[0]);
|
|
174
|
+
}
|
|
175
|
+
return '<a class="squiffy-link" data-' + type + '="' + rotation[1] + '" data-attribute="' + attribute + '" role="link">' + rotation[0] + '</a>';
|
|
176
|
+
}
|
|
177
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {SquiffyEventHandler, SquiffyEventMap} from "./events.js";
|
|
2
|
+
|
|
3
|
+
export interface SquiffyInitOptions {
|
|
4
|
+
element: HTMLElement;
|
|
5
|
+
story: Story;
|
|
6
|
+
scroll?: string,
|
|
7
|
+
persist?: boolean,
|
|
8
|
+
onSet?: (attribute: string, value: any) => void,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SquiffySettings {
|
|
12
|
+
scroll: string,
|
|
13
|
+
persist: boolean,
|
|
14
|
+
onSet: (attribute: string, value: any) => void,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SquiffyApi {
|
|
18
|
+
restart: () => void;
|
|
19
|
+
get: (attribute: string) => any;
|
|
20
|
+
set: (attribute: string, value: any) => void;
|
|
21
|
+
clickLink: (link: HTMLElement) => boolean;
|
|
22
|
+
update: (story: Story) => void;
|
|
23
|
+
|
|
24
|
+
on<E extends keyof SquiffyEventMap>(
|
|
25
|
+
event: E,
|
|
26
|
+
handler: SquiffyEventHandler<E>
|
|
27
|
+
): () => void; // returns unsubscribe
|
|
28
|
+
|
|
29
|
+
off<E extends keyof SquiffyEventMap>(
|
|
30
|
+
event: E,
|
|
31
|
+
handler: SquiffyEventHandler<E>
|
|
32
|
+
): void;
|
|
33
|
+
|
|
34
|
+
once<E extends keyof SquiffyEventMap>(
|
|
35
|
+
event: E,
|
|
36
|
+
handler: SquiffyEventHandler<E>
|
|
37
|
+
): () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Previous versions of Squiffy had "squiffy", "get" and "set" as globals - we now pass these directly into JS functions.
|
|
41
|
+
// We may tidy up this API at some point, though that would be a breaking change.
|
|
42
|
+
interface SquiffyJsFunctionApi {
|
|
43
|
+
get: (attribute: string) => any;
|
|
44
|
+
set: (attribute: string, value: any) => void;
|
|
45
|
+
ui: {
|
|
46
|
+
transition: (f: any) => void;
|
|
47
|
+
};
|
|
48
|
+
story: {
|
|
49
|
+
go: (section: string) => void;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface Story {
|
|
54
|
+
js: ((
|
|
55
|
+
squiffy: SquiffyJsFunctionApi,
|
|
56
|
+
get: (attribute: string) => any,
|
|
57
|
+
set: (attribute: string, value: any) => void
|
|
58
|
+
) => void)[];
|
|
59
|
+
start: string;
|
|
60
|
+
id?: string | null;
|
|
61
|
+
sections: Record<string, Section>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface Section {
|
|
65
|
+
text?: string;
|
|
66
|
+
clear?: boolean;
|
|
67
|
+
attributes?: string[],
|
|
68
|
+
jsIndex?: number;
|
|
69
|
+
passages?: Record<string, Passage>;
|
|
70
|
+
passageCount?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface Passage {
|
|
74
|
+
text?: string;
|
|
75
|
+
clear?: boolean;
|
|
76
|
+
attributes?: string[];
|
|
77
|
+
jsIndex?: number;
|
|
78
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function startsWith(string: string, prefix: string) {
|
|
2
|
+
return string.substring(0, prefix.length) === prefix;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function rotate(options: string, current: string | null) {
|
|
6
|
+
const colon = options.indexOf(':');
|
|
7
|
+
if (colon == -1) {
|
|
8
|
+
return [options, current];
|
|
9
|
+
}
|
|
10
|
+
const next = options.substring(0, colon);
|
|
11
|
+
let remaining = options.substring(colon + 1);
|
|
12
|
+
if (current) remaining += ':' + current;
|
|
13
|
+
return [next, remaining];
|
|
14
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
"noFallthroughCasesInSwitch": true,
|
|
8
8
|
"skipLibCheck": true,
|
|
9
9
|
"outDir": "dist",
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"module": "NodeNext",
|
|
12
|
+
"moduleResolution": "nodenext",
|
|
13
|
+
"allowSyntheticDefaultImports": true
|
|
10
14
|
},
|
|
11
|
-
"include": ["src
|
|
15
|
+
"include": ["src/*.ts"]
|
|
12
16
|
}
|
|
13
17
|
|