squiffy-runtime 6.0.0-alpha.15 → 6.0.0-alpha.17
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/animation.d.ts +11 -0
- package/dist/events.d.ts +8 -3
- package/dist/import.d.ts +4 -0
- package/dist/linkHandler.d.ts +8 -0
- package/dist/pluginManager.d.ts +23 -0
- package/dist/squiffy.runtime.d.ts +2 -2
- package/dist/squiffy.runtime.global.js +126 -0
- package/dist/squiffy.runtime.global.js.map +1 -0
- package/dist/squiffy.runtime.js +8785 -487
- package/dist/squiffy.runtime.js.map +1 -0
- package/dist/state.d.ts +19 -0
- package/dist/textProcessor.d.ts +8 -13
- package/dist/types.d.ts +5 -2
- package/dist/types.plugins.d.ts +27 -0
- package/dist/updater.d.ts +2 -0
- package/dist/utils.d.ts +1 -2
- package/package.json +12 -5
- package/src/__snapshots__/squiffy.runtime.test.ts.snap +53 -19
- package/src/animation.ts +68 -0
- package/src/events.ts +9 -10
- package/src/import.ts +5 -0
- package/src/linkHandler.ts +18 -0
- package/src/pluginManager.ts +74 -0
- package/src/plugins/animate.ts +97 -0
- package/src/plugins/index.ts +13 -0
- package/src/plugins/live.ts +83 -0
- package/src/plugins/random.ts +22 -0
- package/src/plugins/replaceLabel.ts +22 -0
- package/src/plugins/rotateSequence.ts +61 -0
- package/src/squiffy.runtime.test.ts +306 -134
- package/src/squiffy.runtime.ts +460 -332
- package/src/state.ts +106 -0
- package/src/textProcessor.ts +61 -164
- package/src/types.plugins.ts +41 -0
- package/src/types.ts +5 -2
- package/src/updater.ts +77 -0
- package/src/utils.ts +15 -12
- package/vite.config.ts +36 -0
- package/vitest.config.ts +9 -0
- package/vitest.setup.ts +16 -0
- package/dist/events.js +0 -35
- package/dist/squiffy.runtime.test.js +0 -394
- package/dist/textProcessor.js +0 -166
- package/dist/types.js +0 -1
- package/dist/utils.js +0 -14
package/src/state.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Emitter, SquiffyEventMap } from "./events.js";
|
|
2
|
+
|
|
3
|
+
export class State {
|
|
4
|
+
persist: boolean;
|
|
5
|
+
storyId: string;
|
|
6
|
+
onSet: (attribute: string, value: any) => void;
|
|
7
|
+
onSetInternal?: (attribute: string, oldValue: any, newValue: any) => void;
|
|
8
|
+
store: Record<string, any> = {};
|
|
9
|
+
emitter: Emitter<SquiffyEventMap>;
|
|
10
|
+
|
|
11
|
+
constructor(persist: boolean,
|
|
12
|
+
storyId: string,
|
|
13
|
+
onSet: (attribute: string, value: any) => void,
|
|
14
|
+
emitter: Emitter<SquiffyEventMap>,
|
|
15
|
+
onSetInternal?: (attribute: string, oldValue: any, newValue: any) => void) {
|
|
16
|
+
this.persist = persist;
|
|
17
|
+
this.storyId = storyId;
|
|
18
|
+
this.onSet = onSet;
|
|
19
|
+
this.emitter = emitter;
|
|
20
|
+
this.onSetInternal = onSetInternal;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private usePersistentStorage() {
|
|
24
|
+
return this.persist && window.localStorage && this.storyId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
set(attribute: string, value: any) {
|
|
28
|
+
this.setInternal(attribute, value, true);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
setInternal(attribute: string, value: any, raiseEvents: boolean) {
|
|
32
|
+
if (typeof value === "undefined") value = true;
|
|
33
|
+
|
|
34
|
+
if (raiseEvents && this.onSetInternal) {
|
|
35
|
+
this.onSetInternal(attribute, this.get(attribute), structuredClone(value));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.store[attribute] = structuredClone(value);
|
|
39
|
+
|
|
40
|
+
if (this.usePersistentStorage()) {
|
|
41
|
+
localStorage[this.storyId + "-" + attribute] = JSON.stringify(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (raiseEvents) {
|
|
45
|
+
this.emitter.emit("set", { attribute, value });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.onSet(attribute, value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get(attribute: string): any {
|
|
52
|
+
if (attribute in this.store) {
|
|
53
|
+
return structuredClone(this.store[attribute]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getStore() {
|
|
60
|
+
return structuredClone(this.store);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
load() {
|
|
64
|
+
if (!this.usePersistentStorage()) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const keys = Object.keys(localStorage);
|
|
69
|
+
for (const key of keys) {
|
|
70
|
+
if (key.startsWith(this.storyId + "-")) {
|
|
71
|
+
const attribute = key.substring(this.storyId.length + 1);
|
|
72
|
+
this.store[attribute] = JSON.parse(localStorage[key]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
reset() {
|
|
78
|
+
this.store = {};
|
|
79
|
+
|
|
80
|
+
if (!this.usePersistentStorage()) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const keys = Object.keys(localStorage);
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
if (key.startsWith(this.storyId)) {
|
|
87
|
+
localStorage.removeItem(key);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setSeen(sectionName: string) {
|
|
93
|
+
let seenSections = this.get("_seen_sections");
|
|
94
|
+
if (!seenSections) seenSections = [];
|
|
95
|
+
if (seenSections.indexOf(sectionName) == -1) {
|
|
96
|
+
seenSections.push(sectionName);
|
|
97
|
+
this.set("_seen_sections", seenSections);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getSeen(sectionName: string) {
|
|
102
|
+
const seenSections = this.get("_seen_sections");
|
|
103
|
+
if (!seenSections) return false;
|
|
104
|
+
return (seenSections.indexOf(sectionName) > -1);
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/textProcessor.ts
CHANGED
|
@@ -1,179 +1,76 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import * as marked from "marked";
|
|
2
|
+
import Handlebars from "handlebars";
|
|
3
|
+
import { Section, Story } from "./types.js";
|
|
4
|
+
import { State } from "./state.js";
|
|
3
5
|
|
|
4
6
|
export class TextProcessor {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
story: any;
|
|
7
|
+
story: Story;
|
|
8
|
+
state: State;
|
|
8
9
|
getCurrentSection: () => Section;
|
|
9
|
-
|
|
10
|
-
processAttributes: (attributes: string[]) => void;
|
|
10
|
+
handlebars: typeof Handlebars;
|
|
11
11
|
|
|
12
|
-
constructor (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
seen: (section: string) => boolean,
|
|
16
|
-
processAttributes: (attributes: string[]) => void) {
|
|
17
|
-
this.get = get;
|
|
18
|
-
this.set = set;
|
|
12
|
+
constructor (story: Story,
|
|
13
|
+
state: State,
|
|
14
|
+
currentSection: () => Section) {
|
|
19
15
|
this.story = story;
|
|
16
|
+
this.state = state;
|
|
20
17
|
this.getCurrentSection = currentSection;
|
|
21
|
-
this.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (open > -1) {
|
|
31
|
-
let nestCount = 1;
|
|
32
|
-
let searchStart = open + 1;
|
|
33
|
-
let finished = false;
|
|
34
|
-
|
|
35
|
-
while (!finished) {
|
|
36
|
-
const nextOpen = text.indexOf('{', searchStart);
|
|
37
|
-
const nextClose = text.indexOf('}', searchStart);
|
|
38
|
-
|
|
39
|
-
if (nextClose > -1) {
|
|
40
|
-
if (nextOpen > -1 && nextOpen < nextClose) {
|
|
41
|
-
nestCount++;
|
|
42
|
-
searchStart = nextOpen + 1;
|
|
43
|
-
} else {
|
|
44
|
-
nestCount--;
|
|
45
|
-
searchStart = nextClose + 1;
|
|
46
|
-
if (nestCount === 0) {
|
|
47
|
-
close = nextClose;
|
|
48
|
-
containsUnprocessedSection = true;
|
|
49
|
-
finished = true;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
} else {
|
|
53
|
-
finished = true;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (containsUnprocessedSection) {
|
|
59
|
-
const section = text.substring(open + 1, close);
|
|
60
|
-
const value = this.processTextCommand(section, data);
|
|
61
|
-
text = text.substring(0, open) + value + this.process(text.substring(close! + 1), data);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return (text);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
processTextCommand(text: string, data: any) {
|
|
68
|
-
const currentSection = this.getCurrentSection();
|
|
69
|
-
if (startsWith(text, 'if ')) {
|
|
70
|
-
return this.processTextCommand_If(text, data);
|
|
71
|
-
} else if (startsWith(text, 'else:')) {
|
|
72
|
-
return this.processTextCommand_Else(text, data);
|
|
73
|
-
} else if (startsWith(text, 'label:')) {
|
|
74
|
-
return this.processTextCommand_Label(text, data);
|
|
75
|
-
} else if (/^rotate[: ]/.test(text)) {
|
|
76
|
-
return this.processTextCommand_Rotate('rotate', text);
|
|
77
|
-
} else if (/^sequence[: ]/.test(text)) {
|
|
78
|
-
return this.processTextCommand_Rotate('sequence', text);
|
|
79
|
-
} else if (currentSection.passages && text in currentSection.passages) {
|
|
80
|
-
return this.process(currentSection.passages[text].text || '', data);
|
|
81
|
-
} else if (text in this.story.sections) {
|
|
82
|
-
return this.process(this.story.sections[text].text || '', data);
|
|
83
|
-
} else if (startsWith(text, '@') && !startsWith(text, '@replace')) {
|
|
84
|
-
this.processAttributes(text.substring(1).split(","));
|
|
85
|
-
return "";
|
|
86
|
-
}
|
|
87
|
-
return this.get(text);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
processTextCommand_If(section: string, data: any) {
|
|
91
|
-
const command = section.substring(3);
|
|
92
|
-
const colon = command.indexOf(':');
|
|
93
|
-
if (colon == -1) {
|
|
94
|
-
return ('{if ' + command + '}');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const text = command.substring(colon + 1);
|
|
98
|
-
let condition = command.substring(0, colon);
|
|
99
|
-
condition = condition.replace("<", "<");
|
|
100
|
-
const operatorRegex = /([\w ]*)(=|<=|>=|<>|<|>)(.*)/;
|
|
101
|
-
const match = operatorRegex.exec(condition);
|
|
102
|
-
|
|
103
|
-
let result = false;
|
|
104
|
-
|
|
105
|
-
if (match) {
|
|
106
|
-
const lhs = this.get(match[1]);
|
|
107
|
-
const op = match[2];
|
|
108
|
-
let rhs = match[3];
|
|
109
|
-
|
|
110
|
-
if (startsWith(rhs, '@')) rhs = this.get(rhs.substring(1));
|
|
111
|
-
|
|
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
|
-
if (op == '>=' && lhs >= rhs) result = true;
|
|
117
|
-
if (op == '<=' && lhs <= rhs) result = true;
|
|
118
|
-
} else {
|
|
119
|
-
let checkValue = true;
|
|
120
|
-
if (startsWith(condition, 'not ')) {
|
|
121
|
-
condition = condition.substring(4);
|
|
122
|
-
checkValue = false;
|
|
18
|
+
this.handlebars = Handlebars.create();
|
|
19
|
+
|
|
20
|
+
this.handlebars.registerHelper("embed", (name: string) => {
|
|
21
|
+
const currentSection = this.getCurrentSection();
|
|
22
|
+
if (currentSection.passages && name in currentSection.passages) {
|
|
23
|
+
return this.process(currentSection.passages[name].text || "", true);
|
|
24
|
+
} else if (name in this.story.sections) {
|
|
25
|
+
return this.process(this.story.sections[name].text || "", true);
|
|
123
26
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
this.handlebars.registerHelper("seen", (name: string) => this.state.getSeen(name));
|
|
30
|
+
this.handlebars.registerHelper("get", (attribute: string) => this.state.get(attribute));
|
|
31
|
+
this.handlebars.registerHelper("and", (...args) => args.slice(0,-1).every(Boolean));
|
|
32
|
+
this.handlebars.registerHelper("or", (...args) => args.slice(0,-1).some(Boolean));
|
|
33
|
+
this.handlebars.registerHelper("not", (v) => !v);
|
|
34
|
+
this.handlebars.registerHelper("eq", (a,b) => a == b);
|
|
35
|
+
this.handlebars.registerHelper("ne", (a,b) => a != b);
|
|
36
|
+
this.handlebars.registerHelper("gt", (a,b) => a > b);
|
|
37
|
+
this.handlebars.registerHelper("lt", (a,b) => a < b);
|
|
38
|
+
this.handlebars.registerHelper("gte", (a,b) => a >= b);
|
|
39
|
+
this.handlebars.registerHelper("lte", (a,b) => a <= b);
|
|
40
|
+
this.handlebars.registerHelper("array", function (...args) {
|
|
41
|
+
args.pop(); // remove last argument - options
|
|
42
|
+
return args;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const addAdditionalParameters = (options: any) => {
|
|
46
|
+
let result = "";
|
|
47
|
+
const setters = options.hash.set as string || "";
|
|
48
|
+
if (setters) {
|
|
49
|
+
result += ` data-set='${JSON.stringify(setters.split(",").map(s => s.trim()))}'`;
|
|
131
50
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
this.handlebars.registerHelper("section", (section: string, options) => {
|
|
55
|
+
const text = options.hash.text as string || section;
|
|
56
|
+
return new Handlebars.SafeString(`<a class="squiffy-link link-section" data-section="${section}"${addAdditionalParameters(options)} role="link" tabindex="0">${text}</a>`);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.handlebars.registerHelper("passage", (passage: string, options) => {
|
|
60
|
+
const text = options.hash.text as string || passage;
|
|
61
|
+
return new Handlebars.SafeString(`<a class="squiffy-link link-passage" data-passage="${passage}"${addAdditionalParameters(options)} role="link" tabindex="0">${text}</a>`);
|
|
62
|
+
});
|
|
144
63
|
}
|
|
145
64
|
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
if (eq == -1) {
|
|
150
|
-
return ('{label:' + command + '}');
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const text = command.substring(eq + 1);
|
|
154
|
-
const label = command.substring(0, eq);
|
|
155
|
-
|
|
156
|
-
return '<span class="squiffy-label-' + label + '">' + this.process(text, data) + '</span>';
|
|
157
|
-
}
|
|
65
|
+
process(text: string, inline: boolean) {
|
|
66
|
+
const template = this.handlebars.compile(text);
|
|
67
|
+
text = template(this.state.getStore());
|
|
158
68
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
let attribute = '';
|
|
162
|
-
if (section.substring(type.length, type.length + 1) == ' ') {
|
|
163
|
-
const colon = section.indexOf(':');
|
|
164
|
-
if (colon == -1) {
|
|
165
|
-
return '{' + section + '}';
|
|
166
|
-
}
|
|
167
|
-
options = section.substring(colon + 1);
|
|
168
|
-
attribute = section.substring(type.length + 1, colon);
|
|
169
|
-
} else {
|
|
170
|
-
options = section.substring(type.length + 1);
|
|
69
|
+
if (inline) {
|
|
70
|
+
return marked.parseInline(text, { async: false }).trim();
|
|
171
71
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (attribute) {
|
|
175
|
-
this.set(attribute, rotation[0]);
|
|
72
|
+
else {
|
|
73
|
+
return marked.parse(text, { async: false }).trim();
|
|
176
74
|
}
|
|
177
|
-
return '<a class="squiffy-link" data-' + type + '="' + rotation[1] + '" data-attribute="' + attribute + '" role="link">' + rotation[0] + '</a>';
|
|
178
75
|
}
|
|
179
76
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { HelperDelegate } from "handlebars";
|
|
2
|
+
import { SquiffyEventHandler, SquiffyEventMap } from "./events.js";
|
|
3
|
+
import { Animation } from "./animation.js";
|
|
4
|
+
|
|
5
|
+
export interface SquiffyPlugin {
|
|
6
|
+
name: string;
|
|
7
|
+
init(host: PluginHost): void | Promise<void>;
|
|
8
|
+
onWrite?(el: HTMLElement): void;
|
|
9
|
+
onLoad?(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HandleLinkResult {
|
|
13
|
+
disableLink?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PluginHost {
|
|
17
|
+
outputElement: HTMLElement;
|
|
18
|
+
registerHelper(name: string, helper: HelperDelegate): void;
|
|
19
|
+
registerLinkHandler(type: string, handler: (el: HTMLElement) => HandleLinkResult): void;
|
|
20
|
+
get(attribute: string): any;
|
|
21
|
+
set(attribute: string, value: any): void;
|
|
22
|
+
getSectionText(name: string): string | null;
|
|
23
|
+
getPassageText(name: string): string | null;
|
|
24
|
+
processText: (text: string, inline: boolean) => string;
|
|
25
|
+
addTransition: (fn: () => Promise<void>) => void;
|
|
26
|
+
animation: Animation;
|
|
27
|
+
on<E extends keyof SquiffyEventMap>(
|
|
28
|
+
event: E,
|
|
29
|
+
handler: SquiffyEventHandler<E>
|
|
30
|
+
): () => void; // returns unsubscribe
|
|
31
|
+
|
|
32
|
+
off<E extends keyof SquiffyEventMap>(
|
|
33
|
+
event: E,
|
|
34
|
+
handler: SquiffyEventHandler<E>
|
|
35
|
+
): void;
|
|
36
|
+
|
|
37
|
+
once<E extends keyof SquiffyEventMap>(
|
|
38
|
+
event: E,
|
|
39
|
+
handler: SquiffyEventHandler<E>
|
|
40
|
+
): () => void;
|
|
41
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {SquiffyEventHandler, SquiffyEventMap} from "./events.js";
|
|
1
|
+
import { SquiffyEventHandler, SquiffyEventMap } from "./events.js";
|
|
2
2
|
|
|
3
3
|
export interface SquiffyInitOptions {
|
|
4
4
|
element: HTMLElement;
|
|
@@ -15,11 +15,13 @@ export interface SquiffySettings {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface SquiffyApi {
|
|
18
|
+
begin: () => Promise<void>;
|
|
18
19
|
restart: () => void;
|
|
19
20
|
get: (attribute: string) => any;
|
|
20
21
|
set: (attribute: string, value: any) => void;
|
|
21
|
-
clickLink: (link: HTMLElement) => boolean
|
|
22
|
+
clickLink: (link: HTMLElement) => Promise<boolean>;
|
|
22
23
|
update: (story: Story) => void;
|
|
24
|
+
goBack: () => void;
|
|
23
25
|
|
|
24
26
|
on<E extends keyof SquiffyEventMap>(
|
|
25
27
|
event: E,
|
|
@@ -58,6 +60,7 @@ export interface Story {
|
|
|
58
60
|
) => void)[];
|
|
59
61
|
start: string;
|
|
60
62
|
id?: string | null;
|
|
63
|
+
uiJsIndex?: number;
|
|
61
64
|
sections: Record<string, Section>;
|
|
62
65
|
}
|
|
63
66
|
|
package/src/updater.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Story } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export function updateStory(oldStory: Story,
|
|
4
|
+
newStory: Story,
|
|
5
|
+
outputElement: HTMLElement,
|
|
6
|
+
ui: any,
|
|
7
|
+
disableLink: (link: Element) => void) {
|
|
8
|
+
|
|
9
|
+
function safeQuerySelector(name: string) {
|
|
10
|
+
return name.replace(/'/g, "\\'");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getSectionContent(section: string) {
|
|
14
|
+
return outputElement.querySelectorAll(`[data-source='[[${safeQuerySelector(section)}]]']`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getPassageContent(section: string, passage: string) {
|
|
18
|
+
return outputElement.querySelectorAll(`[data-source='[[${safeQuerySelector(section)}]][${safeQuerySelector(passage)}]']`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function updateElementTextPreservingDisabledPassageLinks(element: Element, text: string) {
|
|
22
|
+
// Record which passage links are disabled
|
|
23
|
+
const disabledPassages = Array.from(element
|
|
24
|
+
.querySelectorAll("a.link-passage.disabled"))
|
|
25
|
+
.map((el: HTMLElement) => el.getAttribute("data-passage"));
|
|
26
|
+
|
|
27
|
+
element.innerHTML = text;
|
|
28
|
+
|
|
29
|
+
// Re-disable links that were disabled before the update
|
|
30
|
+
for (const passage of disabledPassages) {
|
|
31
|
+
const link = element.querySelector(`a.link-passage[data-passage="${passage}"]`);
|
|
32
|
+
if (link) disableLink(link);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const existingSection of Object.keys(oldStory.sections)) {
|
|
37
|
+
const elements = getSectionContent(existingSection);
|
|
38
|
+
if (elements.length) {
|
|
39
|
+
const newSection = newStory.sections[existingSection];
|
|
40
|
+
if (!newSection) {
|
|
41
|
+
// section has been deleted
|
|
42
|
+
for (const element of elements) {
|
|
43
|
+
const parentOutputSection = element.closest(".squiffy-output-section");
|
|
44
|
+
parentOutputSection.remove();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (newSection.text != oldStory.sections[existingSection].text) {
|
|
48
|
+
// section has been updated
|
|
49
|
+
for (const element of elements) {
|
|
50
|
+
updateElementTextPreservingDisabledPassageLinks(element, ui.processText(newSection.text, false));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!oldStory.sections[existingSection].passages) continue;
|
|
56
|
+
|
|
57
|
+
for (const existingPassage of Object.keys(oldStory.sections[existingSection].passages)) {
|
|
58
|
+
const elements = getPassageContent(existingSection, existingPassage);
|
|
59
|
+
if (!elements.length) continue;
|
|
60
|
+
|
|
61
|
+
const newPassage = newStory.sections[existingSection]?.passages && newStory.sections[existingSection]?.passages[existingPassage];
|
|
62
|
+
if (!newPassage) {
|
|
63
|
+
// passage has been deleted
|
|
64
|
+
for (const element of elements) {
|
|
65
|
+
const parentOutputPassage = element.closest(".squiffy-output-passage");
|
|
66
|
+
parentOutputPassage.remove();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (newPassage.text && newPassage.text != oldStory.sections[existingSection].passages[existingPassage].text) {
|
|
70
|
+
// passage has been updated
|
|
71
|
+
for (const element of elements) {
|
|
72
|
+
updateElementTextPreservingDisabledPassageLinks(element, ui.processText(newPassage.text, false));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
export function
|
|
2
|
-
return
|
|
3
|
-
|
|
1
|
+
export function fadeReplace(element: HTMLElement, text: string): Promise<void> {
|
|
2
|
+
return new Promise((resolve) => {
|
|
3
|
+
element.addEventListener("transitionend", function () {
|
|
4
|
+
element.innerHTML = text;
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
element.addEventListener("transitionend", function () {
|
|
7
|
+
element.classList.remove("fade-in");
|
|
8
|
+
resolve();
|
|
9
|
+
}, { once: true });
|
|
10
|
+
|
|
11
|
+
element.classList.remove("fade-out");
|
|
12
|
+
element.classList.add("fade-in");
|
|
13
|
+
}, { once: true });
|
|
14
|
+
|
|
15
|
+
element.classList.add("fade-out");
|
|
16
|
+
});
|
|
14
17
|
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import dts from "vite-plugin-dts";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
build: {
|
|
6
|
+
target: "es2020",
|
|
7
|
+
sourcemap: true,
|
|
8
|
+
minify: "esbuild",
|
|
9
|
+
|
|
10
|
+
lib: {
|
|
11
|
+
entry: "src/squiffy.runtime.ts",
|
|
12
|
+
name: "squiffyRuntime"
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
rollupOptions: {
|
|
16
|
+
output: [
|
|
17
|
+
{
|
|
18
|
+
format: "es",
|
|
19
|
+
entryFileNames: "squiffy.runtime.js"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
format: "iife",
|
|
23
|
+
name: "squiffyRuntime",
|
|
24
|
+
entryFileNames: "squiffy.runtime.global.js"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
plugins: [
|
|
31
|
+
dts({
|
|
32
|
+
entryRoot: "src",
|
|
33
|
+
outDir: "dist"
|
|
34
|
+
})
|
|
35
|
+
]
|
|
36
|
+
});
|
package/vitest.config.ts
ADDED
package/vitest.setup.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import cssEscape from "css.escape";
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
interface CSS {
|
|
5
|
+
escape(value: string): string;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const g = globalThis as any;
|
|
10
|
+
|
|
11
|
+
if (!g.CSS) g.CSS = {};
|
|
12
|
+
if (typeof g.CSS.escape !== "function") {
|
|
13
|
+
g.CSS.escape = cssEscape;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export {};
|
package/dist/events.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
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
|
-
}
|