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.
Files changed (45) hide show
  1. package/dist/animation.d.ts +11 -0
  2. package/dist/events.d.ts +8 -3
  3. package/dist/import.d.ts +4 -0
  4. package/dist/linkHandler.d.ts +8 -0
  5. package/dist/pluginManager.d.ts +23 -0
  6. package/dist/squiffy.runtime.d.ts +2 -2
  7. package/dist/squiffy.runtime.global.js +126 -0
  8. package/dist/squiffy.runtime.global.js.map +1 -0
  9. package/dist/squiffy.runtime.js +8785 -487
  10. package/dist/squiffy.runtime.js.map +1 -0
  11. package/dist/state.d.ts +19 -0
  12. package/dist/textProcessor.d.ts +8 -13
  13. package/dist/types.d.ts +5 -2
  14. package/dist/types.plugins.d.ts +27 -0
  15. package/dist/updater.d.ts +2 -0
  16. package/dist/utils.d.ts +1 -2
  17. package/package.json +12 -5
  18. package/src/__snapshots__/squiffy.runtime.test.ts.snap +53 -19
  19. package/src/animation.ts +68 -0
  20. package/src/events.ts +9 -10
  21. package/src/import.ts +5 -0
  22. package/src/linkHandler.ts +18 -0
  23. package/src/pluginManager.ts +74 -0
  24. package/src/plugins/animate.ts +97 -0
  25. package/src/plugins/index.ts +13 -0
  26. package/src/plugins/live.ts +83 -0
  27. package/src/plugins/random.ts +22 -0
  28. package/src/plugins/replaceLabel.ts +22 -0
  29. package/src/plugins/rotateSequence.ts +61 -0
  30. package/src/squiffy.runtime.test.ts +306 -134
  31. package/src/squiffy.runtime.ts +460 -332
  32. package/src/state.ts +106 -0
  33. package/src/textProcessor.ts +61 -164
  34. package/src/types.plugins.ts +41 -0
  35. package/src/types.ts +5 -2
  36. package/src/updater.ts +77 -0
  37. package/src/utils.ts +15 -12
  38. package/vite.config.ts +36 -0
  39. package/vitest.config.ts +9 -0
  40. package/vitest.setup.ts +16 -0
  41. package/dist/events.js +0 -35
  42. package/dist/squiffy.runtime.test.js +0 -394
  43. package/dist/textProcessor.js +0 -166
  44. package/dist/types.js +0 -1
  45. 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
+ }
@@ -1,179 +1,76 @@
1
- import { startsWith, rotate } from "./utils.js";
2
- import { Section } from "./types.js";
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
- get: (attribute: string) => any;
6
- set: (attribute: string, value: any) => void;
7
- story: any;
7
+ story: Story;
8
+ state: State;
8
9
  getCurrentSection: () => Section;
9
- seen: (section: string) => boolean;
10
- processAttributes: (attributes: string[]) => void;
10
+ handlebars: typeof Handlebars;
11
11
 
12
- constructor (get: (attribute: string) => any,
13
- set: (attribute: string, value: any) => void,
14
- story: any, currentSection: () => Section,
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.seen = seen;
22
- this.processAttributes = processAttributes;
23
- }
24
-
25
- process(text: string, data: any) {
26
- let containsUnprocessedSection = false;
27
- const open = text.indexOf('{');
28
- let close;
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("<", "&lt;");
100
- const operatorRegex = /([\w ]*)(=|&lt;=|&gt;=|&lt;&gt;|&lt;|&gt;)(.*)/;
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 == '&lt;&gt;' && lhs != rhs) result = true;
114
- if (op == '&gt;' && lhs > rhs) result = true;
115
- if (op == '&lt;' && lhs < rhs) result = true;
116
- if (op == '&gt;=' && lhs >= rhs) result = true;
117
- if (op == '&lt;=' && 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
- if (startsWith(condition, 'seen ')) {
126
- result = (this.seen(condition.substring(5)) == checkValue);
127
- } else {
128
- let value = this.get(condition);
129
- if (value === null) value = false;
130
- result = (value == checkValue);
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
- const textResult = result ? this.process(text, data) : '';
135
-
136
- data.lastIf = result;
137
- return textResult;
138
- }
139
-
140
- processTextCommand_Else(section: string, data: any) {
141
- if (!('lastIf' in data) || data.lastIf) return '';
142
- const text = section.substring(5);
143
- return this.process(text, data);
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
- processTextCommand_Label(section: string, data: any) {
147
- const command = section.substring(6);
148
- const eq = command.indexOf('=');
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
- processTextCommand_Rotate(type: string, section: string) {
160
- let options;
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
- // TODO: Check - previously there was no second parameter here
173
- const rotation = rotate(options.replace(/"/g, '&quot;').replace(/'/g, '&#39;'), null);
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 startsWith(string: string, prefix: string) {
2
- return string.substring(0, prefix.length) === prefix;
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
- 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];
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
+ });
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: "jsdom",
7
+ setupFiles: "./vitest.setup.ts",
8
+ },
9
+ });
@@ -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
- }