squiffy-runtime 6.0.0-alpha.14 → 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 (46) hide show
  1. package/LICENSE +22 -0
  2. package/dist/animation.d.ts +11 -0
  3. package/dist/events.d.ts +8 -3
  4. package/dist/import.d.ts +4 -0
  5. package/dist/linkHandler.d.ts +8 -0
  6. package/dist/pluginManager.d.ts +23 -0
  7. package/dist/squiffy.runtime.d.ts +2 -2
  8. package/dist/squiffy.runtime.global.js +126 -0
  9. package/dist/squiffy.runtime.global.js.map +1 -0
  10. package/dist/squiffy.runtime.js +8785 -487
  11. package/dist/squiffy.runtime.js.map +1 -0
  12. package/dist/state.d.ts +19 -0
  13. package/dist/textProcessor.d.ts +8 -13
  14. package/dist/types.d.ts +5 -2
  15. package/dist/types.plugins.d.ts +27 -0
  16. package/dist/updater.d.ts +2 -0
  17. package/dist/utils.d.ts +1 -2
  18. package/package.json +13 -5
  19. package/src/__snapshots__/squiffy.runtime.test.ts.snap +53 -19
  20. package/src/animation.ts +68 -0
  21. package/src/events.ts +9 -10
  22. package/src/import.ts +5 -0
  23. package/src/linkHandler.ts +18 -0
  24. package/src/pluginManager.ts +74 -0
  25. package/src/plugins/animate.ts +97 -0
  26. package/src/plugins/index.ts +13 -0
  27. package/src/plugins/live.ts +83 -0
  28. package/src/plugins/random.ts +22 -0
  29. package/src/plugins/replaceLabel.ts +22 -0
  30. package/src/plugins/rotateSequence.ts +61 -0
  31. package/src/squiffy.runtime.test.ts +306 -134
  32. package/src/squiffy.runtime.ts +460 -332
  33. package/src/state.ts +106 -0
  34. package/src/textProcessor.ts +61 -166
  35. package/src/types.plugins.ts +41 -0
  36. package/src/types.ts +5 -2
  37. package/src/updater.ts +77 -0
  38. package/src/utils.ts +15 -12
  39. package/vite.config.ts +36 -0
  40. package/vitest.config.ts +9 -0
  41. package/vitest.setup.ts +16 -0
  42. package/dist/events.js +0 -35
  43. package/dist/squiffy.runtime.test.js +0 -394
  44. package/dist/textProcessor.js +0 -168
  45. package/dist/types.js +0 -1
  46. 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,181 +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
- console.log("Found passage");
81
- return this.process(currentSection.passages[text].text || '', data);
82
- } else if (text in this.story.sections) {
83
- console.log("Found section");
84
- return this.process(this.story.sections[text].text || '', data);
85
- } else if (startsWith(text, '@') && !startsWith(text, '@replace')) {
86
- this.processAttributes(text.substring(1).split(","));
87
- return "";
88
- }
89
- return this.get(text);
90
- }
91
-
92
- processTextCommand_If(section: string, data: any) {
93
- const command = section.substring(3);
94
- const colon = command.indexOf(':');
95
- if (colon == -1) {
96
- return ('{if ' + command + '}');
97
- }
98
-
99
- const text = command.substring(colon + 1);
100
- let condition = command.substring(0, colon);
101
- condition = condition.replace("<", "&lt;");
102
- const operatorRegex = /([\w ]*)(=|&lt;=|&gt;=|&lt;&gt;|&lt;|&gt;)(.*)/;
103
- const match = operatorRegex.exec(condition);
104
-
105
- let result = false;
106
-
107
- if (match) {
108
- const lhs = this.get(match[1]);
109
- const op = match[2];
110
- let rhs = match[3];
111
-
112
- if (startsWith(rhs, '@')) rhs = this.get(rhs.substring(1));
113
-
114
- if (op == '=' && lhs == rhs) result = true;
115
- if (op == '&lt;&gt;' && lhs != rhs) result = true;
116
- if (op == '&gt;' && lhs > rhs) result = true;
117
- if (op == '&lt;' && lhs < rhs) result = true;
118
- if (op == '&gt;=' && lhs >= rhs) result = true;
119
- if (op == '&lt;=' && lhs <= rhs) result = true;
120
- } else {
121
- let checkValue = true;
122
- if (startsWith(condition, 'not ')) {
123
- condition = condition.substring(4);
124
- 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);
125
26
  }
126
-
127
- if (startsWith(condition, 'seen ')) {
128
- result = (this.seen(condition.substring(5)) == checkValue);
129
- } else {
130
- let value = this.get(condition);
131
- if (value === null) value = false;
132
- 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()))}'`;
133
50
  }
134
- }
135
-
136
- const textResult = result ? this.process(text, data) : '';
137
-
138
- data.lastIf = result;
139
- return textResult;
140
- }
141
-
142
- processTextCommand_Else(section: string, data: any) {
143
- if (!('lastIf' in data) || data.lastIf) return '';
144
- const text = section.substring(5);
145
- 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
+ });
146
63
  }
147
64
 
148
- processTextCommand_Label(section: string, data: any) {
149
- const command = section.substring(6);
150
- const eq = command.indexOf('=');
151
- if (eq == -1) {
152
- return ('{label:' + command + '}');
153
- }
154
-
155
- const text = command.substring(eq + 1);
156
- const label = command.substring(0, eq);
157
-
158
- return '<span class="squiffy-label-' + label + '">' + this.process(text, data) + '</span>';
159
- }
65
+ process(text: string, inline: boolean) {
66
+ const template = this.handlebars.compile(text);
67
+ text = template(this.state.getStore());
160
68
 
161
- processTextCommand_Rotate(type: string, section: string) {
162
- let options;
163
- let attribute = '';
164
- if (section.substring(type.length, type.length + 1) == ' ') {
165
- const colon = section.indexOf(':');
166
- if (colon == -1) {
167
- return '{' + section + '}';
168
- }
169
- options = section.substring(colon + 1);
170
- attribute = section.substring(type.length + 1, colon);
171
- } else {
172
- options = section.substring(type.length + 1);
69
+ if (inline) {
70
+ return marked.parseInline(text, { async: false }).trim();
173
71
  }
174
- // TODO: Check - previously there was no second parameter here
175
- const rotation = rotate(options.replace(/"/g, '&quot;').replace(/'/g, '&#39;'), null);
176
- if (attribute) {
177
- this.set(attribute, rotation[0]);
72
+ else {
73
+ return marked.parse(text, { async: false }).trim();
178
74
  }
179
- return '<a class="squiffy-link" data-' + type + '="' + rotation[1] + '" data-attribute="' + attribute + '" role="link">' + rotation[0] + '</a>';
180
75
  }
181
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
- }